Add initial support for mobile receiving views

This commit is contained in:
Lance Edgar 2017-05-24 00:04:56 -05:00
parent d68bf6b012
commit 5eca2347d5
11 changed files with 712 additions and 14 deletions

View file

@ -39,12 +39,14 @@ $(document).on('pagecreate', function() {
/** /**
* Automatically set focus to certain fields, on various pages * Automatically set focus to certain fields, on various pages
* TODO: this should accept selector params instead of hard-coding..?
*/ */
function setfocus() { function setfocus() {
var el = null; var el = null;
var queries = [ var queries = [
'#username', '#username',
'#new-purchasing-batch-vendor-text', '#new-purchasing-batch-vendor-text',
// '.receiving-upc-search',
]; ];
$.each(queries, function(i, query) { $.each(queries, function(i, query) {
el = $(query); el = $(query);
@ -92,3 +94,60 @@ $(document).on('click', 'form[name="new-purchasing-batch"] [data-role="listview"
$(document).on('click', '#datasync-restart', function() { $(document).on('click', '#datasync-restart', function() {
$(this).button('disable'); $(this).button('disable');
}); });
// handle Enter press for receiving UPC lookup
$(document).on('keydown', '.receiving-upc-search', function(event) {
if (event.which == 13) {
$.mobile.navigate($(this).data('url') + '?upc=' + $(this).val());
}
});
// handle numeric buttons for receiving
// $(document).on('click', '#receiving-quantity-keypad-thingy .ui-btn', function() {
$(document).on('click', '#receiving-quantity-keypad-thingy .keypad-button', function() {
var quantity = $('.receiving-quantity');
var value = quantity.text();
var key = $(this).text();
if (key == 'Del') {
if (value.length == 1) {
quantity.text('0');
} else {
quantity.text(value.substring(0, value.length - 1));
}
} else if (key == '.') {
if (value.indexOf('.') == -1) {
quantity.text(value + '.');
}
} else {
if (value == '0') {
quantity.text(key);
} else {
quantity.text(value + key);
}
}
});
// handle receiving action buttons
$(document).on('click', '.receiving-actions button', function() {
var action = $(this).data('action');
var form = $('form.receiving-update');
var uom = form.find('[name="receiving-uom"]').val();
var qty = form.find('.receiving-quantity').text();
if (action == 'add' || action == 'subtract') {
if (qty != '0') {
if (action == 'subtract') {
qty = '-' + qty;
}
if (uom == 'CS') {
form.find('[name="cases"]').val(qty);
} else { // units
form.find('[name="units"]').val(qty);
}
form.submit();
}
}
});

View file

@ -0,0 +1,4 @@
## -*- coding: utf-8; -*-
<%inherit file="/mobile/master/view.mako" />
${parent.body()}

View file

@ -0,0 +1,48 @@
## -*- coding: utf-8; -*-
<%inherit file="/mobile/base.mako" />
<%def name="title()">${h.link_to("Receiving", url('mobile.receiving'))} &raquo; New Batch</%def>
${h.form(request.current_route_url(), class_='ui-filterable', name='new-purchasing-batch')}
${h.csrf_token(request)}
% if vendor is Undefined:
<div class="field-wrapper vendor">
<div class="field autocomplete" data-url="${url('vendors.autocomplete')}">
${h.hidden('vendor')}
${h.text('new-purchasing-batch-vendor-text', placeholder="Vendor name", autocomplete='off', **{'data-type': 'search'})}
<ul data-role="listview" data-inset="true" data-filter="true" data-input="#new-purchasing-batch-vendor-text"></ul>
<button type="button" style="display: none;">Change Vendor</button>
</div>
</div>
<br />
${h.submit('submit', "Find purchase orders")}
## <button type="button">New receiving from scratch</button>
% else: ## vendor is known
<div class="field-wrapper vendor">
<div class="field">
${h.hidden('vendor', value=vendor.uuid)}
${vendor}
</div>
</div>
% if purchases:
${h.hidden('purchase')}
<ul data-role="listview" data-inset="true">
% for uuid, purchase in purchases:
<li data-uuid="${uuid}">${h.link_to(purchase, '#')}</li>
% endfor
</ul>
% else:
<p>(no eligible purchases found)</p>
% endif
## ${h.link_to("Receive from scratch for {}".format(vendor), '#', class_='ui-btn ui-corner-all')}
% endif
${h.end_form()}

View file

@ -0,0 +1,18 @@
## -*- coding: utf-8; -*-
<%inherit file="/mobile/master/index.mako" />
<%def name="title()">Receiving</%def>
% if request.has_perm('receiving.create'):
${h.link_to("New Receiving Batch", url('mobile.receiving.create'), class_='ui-btn ui-corner-all')}
% endif
<fieldset data-role="controlgroup" data-type="horizontal">
${h.radio('receiving-filter', value='pending', label="Pending", checked=True)}
${h.radio('receiving-filter', value='complete', label="Complete", disabled='disabled')}
${h.radio('receiving-filter', value='executed', label="Executed", disabled='disabled')}
${h.radio('receiving-filter', value='all', label="All", disabled='disabled')}
</fieldset>
<br /><br />
${parent.body()}

View file

@ -0,0 +1,25 @@
## -*- coding: utf-8; -*-
<%inherit file="/mobile/master/view.mako" />
<%def name="title()">${h.link_to("Receiving", url('mobile.receiving'))} &raquo; ${instance.id_str}</%def>
${form.render()|n}
<br />
${h.text('upc-search', class_='receiving-upc-search', placeholder="Enter UPC", autocomplete='off', **{'data-type': 'search', 'data-url': url('mobile.receiving.lookup', uuid=batch.uuid)})}
<br />
<fieldset data-role="controlgroup" data-type="horizontal">
## ${h.radio('receiving-row-filter', value='missing', label="Missing", disabled='disabled')}
${h.radio('receiving-row-filter', value='incomplete', label="Incomplete", disabled='disabled')}
${h.radio('receiving-row-filter', value='damaged', label="Damaged", disabled='disabled')}
${h.radio('receiving-row-filter', value='expired', label="Expired", disabled='disabled')}
${h.radio('receiving-row-filter', value='all', label="All", checked=True)}
</fieldset>
<br /><br />
<ul data-role="listview">
% for obj in grid.iter_rows():
<li>${grid.listitem.render_readonly()}</li>
% endfor
</ul>

View file

@ -0,0 +1,107 @@
## -*- coding: utf-8; -*-
<%inherit file="/mobile/master/view_row.mako" />
## TODO: this is broken for actual page (header) title
<%def name="title()">${h.link_to("Receiving", url('mobile.receiving'))} &raquo; ${h.link_to(instance.batch.id_str, url('mobile.receiving.view', uuid=instance.batch_uuid))} &raquo; ${row.upc.pretty()}</%def>
<% unit_uom = 'LB' if row.product.weighed else 'EA' %>
<div class="ui-grid-a">
<div class="ui-block-a">
<h3>${instance.brand_name}</h3>
<h3>${instance.description} ${instance.size}</h3>
<h3>${h.pretty_quantity(row.case_quantity)} ${unit_uom} per CS</h3>
</div>
<div class="ui-block-b">
${h.image(product_image_url, "product image")}
</div>
</div>
<table>
<tbody>
<tr>
<td>ordered</td>
<td>${h.pretty_quantity(row.cases_ordered or 0)} / ${h.pretty_quantity(row.units_ordered or 0)}</td>
</tr>
<tr>
<td>received</td>
<td>${h.pretty_quantity(row.cases_received or 0)} / ${h.pretty_quantity(row.units_received or 0)}</td>
</tr>
<tr>
<td>damaged</td>
<td>${h.pretty_quantity(row.cases_damaged or 0)} / ${h.pretty_quantity(row.units_damaged or 0)}</td>
</tr>
<tr>
<td>expired</td>
<td>${h.pretty_quantity(row.cases_expired or 0)} / ${h.pretty_quantity(row.units_expired or 0)}</td>
</tr>
</tbody>
</table>
<table id="receiving-quantity-keypad-thingy">
<tbody>
<tr>
<td>${h.link_to("7", '#', class_='keypad-button ui-btn ui-btn-inline ui-corner-all')}</td>
<td>${h.link_to("8", '#', class_='keypad-button ui-btn ui-btn-inline ui-corner-all')}</td>
<td>${h.link_to("9", '#', class_='keypad-button ui-btn ui-btn-inline ui-corner-all')}</td>
</tr>
<tr>
<td>${h.link_to("4", '#', class_='keypad-button ui-btn ui-btn-inline ui-corner-all')}</td>
<td>${h.link_to("5", '#', class_='keypad-button ui-btn ui-btn-inline ui-corner-all')}</td>
<td>${h.link_to("6", '#', class_='keypad-button ui-btn ui-btn-inline ui-corner-all')}</td>
</tr>
<tr>
<td>${h.link_to("1", '#', class_='keypad-button ui-btn ui-btn-inline ui-corner-all')}</td>
<td>${h.link_to("2", '#', class_='keypad-button ui-btn ui-btn-inline ui-corner-all')}</td>
<td>${h.link_to("3", '#', class_='keypad-button ui-btn ui-btn-inline ui-corner-all')}</td>
</tr>
<tr>
<td>${h.link_to("0", '#', class_='keypad-button ui-btn ui-btn-inline ui-corner-all')}</td>
<td>${h.link_to(".", '#', class_='keypad-button ui-btn ui-btn-inline ui-corner-all')}</td>
<td>${h.link_to("Del", '#', class_='keypad-button ui-btn ui-btn-inline ui-corner-all')}</td>
</tr>
</tbody>
</table>
${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')}
<%
uom = 'CS'
if row.units_ordered and not row.cases_ordered:
uom = 'EA'
%>
<fieldset data-role="controlgroup" data-type="horizontal">
<button type="button" class="ui-btn-active receiving-quantity">1</button>
<button type="button" disabled="disabled">&nbsp;</button>
${h.radio('receiving-uom', value='CS', checked=uom == 'CS', label="CS")}
${h.radio('receiving-uom', value=unit_uom, checked=uom == unit_uom, label=unit_uom)}
</fieldset>
<table>
<tbody>
<tr>
<td>
<fieldset data-role="controlgroup" data-type="horizontal">
${h.radio('mode', value='received', label="received", checked=True)}
${h.radio('mode', value='damaged', label="damaged")}
${h.radio('mode', value='expired', label="expired")}
</fieldset>
</td>
</tr>
<tr>
<td>
<fieldset data-role="controlgroup" data-type="horizontal" class="receiving-actions">
<button type="button" data-action="add" class="ui-btn-inline ui-corner-all">Add</button>
<button type="button" data-action="subtract" class="ui-btn-inline ui-corner-all">Subtract</button>
<button type="button" data-action="clear" class="ui-btn-inline ui-corner-all ui-state-disabled">Clear</button>
</fieldset>
</td>
</tr>
</tbody>
</table>
${h.end_form()}

View file

@ -60,6 +60,7 @@ class MasterView(View):
downloadable = False downloadable = False
supports_mobile = False supports_mobile = False
mobile_creatable = False
listing = False listing = False
creating = False creating = False
@ -87,6 +88,8 @@ class MasterView(View):
rows_deletable_speedbump = False rows_deletable_speedbump = False
rows_bulk_deletable = False rows_bulk_deletable = False
mobile_rows_viewable = False
@property @property
def Session(self): def Session(self):
""" """
@ -238,6 +241,24 @@ class MasterView(View):
return ListItemRenderer return ListItemRenderer
def mobile_row_listitem_renderer(self):
"""
Must return a FormAlchemy field renderer callable for the mobile row
grid's list item field.
"""
master = self
class ListItemRenderer(fa.FieldRenderer):
def render_readonly(self, **kwargs):
return master.render_mobile_row_listitem(self.raw_value, **kwargs)
return ListItemRenderer
def render_mobile_row_listitem(self, row, **kwargs):
if row is None:
return ''
return tags.link_to(row, '#')
def create(self): def create(self):
""" """
View for creating a new model record. View for creating a new model record.
@ -316,6 +337,9 @@ class MasterView(View):
# 'instance_deletable': self.deletable_instance(instance), # 'instance_deletable': self.deletable_instance(instance),
'form': form, 'form': form,
} }
if self.has_rows:
context['model_row_class'] = self.model_row_class
context['grid'] = self.make_mobile_row_grid(instance=instance)
return self.render_to_response('view', context, mobile=True) return self.render_to_response('view', context, mobile=True)
def make_mobile_form(self, instance, **kwargs): def make_mobile_form(self, instance, **kwargs):
@ -331,6 +355,25 @@ class MasterView(View):
form.readonly = self.viewing form.readonly = self.viewing
return form return form
def preconfigure_mobile_row_fieldset(self, fieldset):
self._preconfigure_row_fieldset(fieldset)
def configure_mobile_row_fieldset(self, fieldset):
self.configure_row_fieldset(fieldset)
def make_mobile_row_form(self, row, **kwargs):
"""
Make a form for use with mobile CRUD views, for the given row object.
"""
fieldset = self.make_fieldset(row)
self.preconfigure_mobile_row_fieldset(fieldset)
self.configure_mobile_row_fieldset(fieldset)
kwargs.setdefault('action_url', self.request.current_route_url(_query=None))
factory = kwargs.pop('factory', forms.AlchemyForm)
form = factory(self.request, fieldset, **kwargs)
form.readonly = self.viewing
return form
def preconfigure_mobile_fieldset(self, fieldset): def preconfigure_mobile_fieldset(self, fieldset):
self._preconfigure_fieldset(fieldset) self._preconfigure_fieldset(fieldset)
@ -340,6 +383,66 @@ class MasterView(View):
""" """
self.configure_fieldset(fieldset) self.configure_fieldset(fieldset)
def get_mobile_row_data(self, parent):
return self.get_row_data(parent)
def make_mobile_row_grid_kwargs(self, **kwargs):
defaults = {
'pageable': True,
'sortable': True,
}
defaults.update(kwargs)
return defaults
def make_mobile_row_grid(self, **kwargs):
"""
Make a new (configured) rows grid instance for mobile.
"""
parent = kwargs.pop('instance', self.get_instance())
kwargs['instance'] = parent
kwargs['data'] = self.get_mobile_row_data(parent)
kwargs['key'] = 'mobile.{}.{}'.format(self.get_grid_key(), self.request.matchdict[self.get_model_key()])
kwargs.setdefault('request', self.request)
kwargs.setdefault('model_class', self.model_row_class)
kwargs = self.make_mobile_row_grid_kwargs(**kwargs)
factory = self.get_grid_factory()
grid = factory(**kwargs)
self.configure_mobile_row_grid(grid)
grid.load_settings()
return grid
def mobile_row_listitem_field(self):
"""
Must return a FormAlchemy field to be appended to row grid, or ``None``
if none is desired.
"""
return fa.Field('listitem', value=lambda obj: obj,
renderer=self.mobile_row_listitem_renderer())
def configure_mobile_row_grid(self, grid):
listitem = self.mobile_row_listitem_field()
if listitem:
grid.append(listitem)
grid.configure(include=[grid.listitem])
else:
grid.configure()
def mobile_view_row(self):
"""
Mobile view for row items
"""
self.viewing = True
row = self.get_row_instance()
form = self.make_mobile_row_form(row)
context = {
'row': row,
'instance': row,
'instance_title': self.get_row_instance_title(row),
'parent_model_title': self.get_model_title(),
'form': form,
}
return self.render_to_response('view_row', context, mobile=True)
def make_default_row_grid_tools(self, obj): def make_default_row_grid_tools(self, obj):
if self.rows_creatable: if self.rows_creatable:
link = tags.link_to("Create a new {}".format(self.get_row_model_title()), link = tags.link_to("Create a new {}".format(self.get_row_model_title()),
@ -1476,12 +1579,17 @@ class MasterView(View):
permission='{}.list'.format(permission_prefix)) permission='{}.list'.format(permission_prefix))
# create # create
if cls.creatable or cls.mobile_creatable:
config.add_tailbone_permission(permission_prefix, '{}.create'.format(permission_prefix),
"Create new {}".format(model_title))
if cls.creatable: if cls.creatable:
config.add_route('{0}.create'.format(route_prefix), '{0}/new'.format(url_prefix)) config.add_route('{}.create'.format(route_prefix), '{}/new'.format(url_prefix))
config.add_view(cls, attr='create', route_name='{0}.create'.format(route_prefix), config.add_view(cls, attr='create', route_name='{}.create'.format(route_prefix),
permission='{0}.create'.format(permission_prefix)) permission='{}.create'.format(permission_prefix))
config.add_tailbone_permission(permission_prefix, '{0}.create'.format(permission_prefix), if cls.mobile_creatable:
"Create new {0}".format(model_title)) config.add_route('mobile.{}.create'.format(route_prefix), '/mobile{}/new'.format(url_prefix))
config.add_view(cls, attr='mobile_create', route_name='mobile.{}.create'.format(route_prefix),
permission='{}.create'.format(permission_prefix))
# bulk delete # bulk delete
if cls.bulk_deletable: if cls.bulk_deletable:
@ -1556,10 +1664,15 @@ class MasterView(View):
"Create new {} rows".format(model_title)) "Create new {} rows".format(model_title))
# view row # view row
if cls.has_rows and cls.rows_viewable: if cls.has_rows:
config.add_route('{}.view'.format(row_route_prefix), '{}/{{uuid}}'.format(row_url_prefix)) if cls.rows_viewable:
config.add_view(cls, attr='view_row', route_name='{}.view'.format(row_route_prefix), config.add_route('{}.view'.format(row_route_prefix), '{}/{{uuid}}'.format(row_url_prefix))
permission='{}.view'.format(permission_prefix)) config.add_view(cls, attr='view_row', route_name='{}.view'.format(row_route_prefix),
permission='{}.view'.format(permission_prefix))
if cls.mobile_rows_viewable:
config.add_route('mobile.{}.view'.format(row_route_prefix), '/mobile{}/{{uuid}}'.format(row_url_prefix))
config.add_view(cls, attr='mobile_view_row', route_name='mobile.{}.view'.format(row_route_prefix),
permission='{}.view'.format(permission_prefix))
# edit row # edit row
if cls.has_rows and cls.rows_editable: if cls.has_rows and cls.rows_editable:

View file

@ -31,3 +31,4 @@ from .batch import PurchasingBatchView
def includeme(config): def includeme(config):
config.include('tailbone.views.purchasing.ordering') config.include('tailbone.views.purchasing.ordering')
config.include('tailbone.views.purchasing.receiving')

View file

@ -46,9 +46,6 @@ class PurchasingBatchView(BatchMasterView):
model_class = model.PurchaseBatch model_class = model.PurchaseBatch
model_row_class = model.PurchaseBatchRow model_row_class = model.PurchaseBatchRow
default_handler_spec = 'rattail.batch.purchase:PurchaseBatchHandler' default_handler_spec = 'rattail.batch.purchase:PurchaseBatchHandler'
rows_creatable = True
rows_editable = True
edit_with_rows = False
@property @property
def batch_mode(self): def batch_mode(self):
@ -530,3 +527,9 @@ class PurchasingBatchView(BatchMasterView):
config.add_route('{}.eligible_purchases'.format(route_prefix), '{}/eligible-purchases'.format(url_prefix)) config.add_route('{}.eligible_purchases'.format(route_prefix), '{}/eligible-purchases'.format(url_prefix))
config.add_view(cls, attr='eligible_purchases', route_name='{}.eligible_purchases'.format(route_prefix), config.add_view(cls, attr='eligible_purchases', route_name='{}.eligible_purchases'.format(route_prefix),
renderer='json', permission='{}.view'.format(permission_prefix)) renderer='json', permission='{}.view'.format(permission_prefix))
@classmethod
def defaults(cls, config):
cls._purchasing_defaults(config)
cls._batch_defaults(config)
cls._defaults(config)

View file

@ -44,8 +44,6 @@ class OrderingBatchView(PurchasingBatchView):
url_prefix = '/ordering' url_prefix = '/ordering'
model_title = "Ordering Batch" model_title = "Ordering Batch"
model_title_plural = "Ordering Batches" model_title_plural = "Ordering Batches"
rows_creatable = False
rows_editable = False
order_form_header_columns = [ order_form_header_columns = [
"UPC", "UPC",

View file

@ -0,0 +1,322 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2017 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 Affero 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 Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Views for 'receiving' (purchasing) batches
"""
from __future__ import unicode_literals, absolute_import
import re
import sqlalchemy as sa
from rattail import pod
from rattail.db import model
from rattail.gpc import GPC
from rattail.util import pretty_quantity
import formalchemy as fa
import formencode as fe
from webhelpers.html import tags
from tailbone import forms
from tailbone.views.purchasing import PurchasingBatchView
class ReceivingBatchView(PurchasingBatchView):
"""
Master view for receiving batches
"""
route_prefix = 'receiving'
url_prefix = '/receiving'
model_title = "Receiving Batch"
model_title_plural = "Receiving Batches"
creatable = False
rows_deletable = False
supports_mobile = True
mobile_creatable = True
mobile_rows_viewable = True
@property
def batch_mode(self):
return self.enum.PURCHASE_BATCH_MODE_RECEIVING
def mobile_create(self):
"""
Mobile view for creating a new receiving batch
"""
mode = self.batch_mode
data = {'mode': mode}
vendor = None
if self.request.method == 'POST' and self.request.POST.get('vendor'):
vendor = self.Session.query(model.Vendor).get(self.request.POST['vendor'])
if vendor:
data['vendor'] = vendor
if self.request.POST.get('purchase'):
purchase = self.get_purchase(self.request.POST['purchase'])
if purchase:
batch = self.model_class()
batch.mode = mode
batch.vendor = vendor
batch.store = self.rattail_config.get_store(self.Session())
batch.buyer = self.request.user.employee
batch.created_by = self.request.user
kwargs = self.get_batch_kwargs(batch, mobile=True)
batch = self.handler.make_batch(self.Session(), **kwargs)
if self.handler.should_populate(batch):
self.handler.populate(batch)
return self.redirect(self.request.route_url('mobile.receiving.view', uuid=batch.uuid))
data['mode_title'] = self.enum.PURCHASE_BATCH_MODE[mode].capitalize()
if vendor:
purchases = self.eligible_purchases(vendor.uuid, mode=mode)
data['purchases'] = [(p['key'], p['display']) for p in purchases['purchases']]
return self.render_to_response('create', data, mobile=True)
def get_batch_kwargs(self, batch, mobile=False):
kwargs = super(ReceivingBatchView, self).get_batch_kwargs(batch, mobile=mobile)
if mobile:
purchase = self.get_purchase(self.request.POST['purchase'])
kwargs['sms_transaction_number'] = purchase.F1032
numbers = [d.F03 for d in purchase.details]
if numbers:
number = max(set(numbers), key=numbers.count)
kwargs['department'] = self.Session.query(model.Department)\
.filter(model.Department.number == number)\
.one()
else:
kwargs['sms_transaction_number'] = batch.sms_transaction_number
return kwargs
def get_mobile_data(self, session=None):
# TODO: this hard-codes list view to show Pending only
return super(ReceivingBatchView, self).get_mobile_data(session=session)\
.filter(model.PurchaseBatch.executed == None)\
.filter(sa.or_(
model.PurchaseBatch.complete == None,
model.PurchaseBatch.complete == False))
def configure_mobile_grid(self, g):
super(ReceivingBatchView, self).configure_mobile_grid(g)
g.listitem.set(renderer=ReceivingBatchRenderer)
def configure_mobile_fieldset(self, fs):
fs.configure(include=[
fs.vendor.with_renderer(fa.TextFieldRenderer),
fs.department.with_renderer(fa.TextFieldRenderer),
])
def get_mobile_row_data(self, batch):
return super(ReceivingBatchView, self).get_mobile_row_data(batch)\
.order_by(model.PurchaseBatchRow.sequence)
def render_mobile_row_listitem(self, row, **kwargs):
if row is None:
return ''
title = "({}) {}".format(row.upc.pretty(), row.product.full_description)
url = self.request.route_url('mobile.receiving.rows.view', uuid=row.uuid)
return tags.link_to(title, url)
def mobile_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()
upc = self.request.GET.get('upc', '').strip()
upc = re.sub(r'\D', '', upc)
if upc:
# first try to locate existing batch row by UPC match
provided = GPC(upc, calc_check_digit=False)
checked = GPC(upc, calc_check_digit='upc')
rows = self.Session.query(model.PurchaseBatchRow)\
.filter(model.PurchaseBatchRow.batch == batch)\
.filter(model.PurchaseBatchRow.upc.in_((provided, checked)))\
.all()
if rows:
if len(rows) > 1:
log.warning("found multiple UPC matches for {} in batch {}: {}".format(
upc, batch.id_str, batch))
row = rows[0]
return self.redirect(self.request.route_url('mobile.{}.view'.format(self.get_row_route_prefix()), uuid=row.uuid))
# TODO: how to handle product not found in system / purchase ?
raise NotImplementedError
def mobile_view_row(self):
"""
Mobile view for receiving batch row items. Note that this also handles
updating a row.
"""
self.viewing = True
row = self.get_row_instance()
form = self.make_mobile_row_form(row)
context = {
'row': row,
'instance': row,
'instance_title': self.get_row_instance_title(row),
'parent_model_title': self.get_model_title(),
'product_image_url': pod.get_image_url(self.rattail_config, row.upc),
'form': form,
}
if self.request.has_perm('{}.edit'.format(self.get_row_permission_prefix())):
update_form = forms.SimpleForm(self.request, schema=ReceivingForm)
if update_form.validate():
row = update_form.data['row']
mode = update_form.data['mode']
cases = update_form.data['cases']
units = update_form.data['units']
if cases:
setattr(row, 'cases_{}'.format(mode),
(getattr(row, 'cases_{}'.format(mode)) or 0) + cases)
if units:
setattr(row, 'units_{}'.format(mode),
(getattr(row, 'units_{}'.format(mode)) or 0) + units)
# if mode in ('damaged', 'expired', 'mispick'):
if mode in ('damaged', 'expired'):
self.attach_credit(row, mode, cases, units,
# expiration_date=update_form.data['expiration_date'],
# discarded=update_form.data['trash'],
# mispick_product=shipped_product)
)
# first undo any totals previously in effect for the row, then refresh
if row.invoice_total:
row.batch.invoice_total -= row.invoice_total
self.handler.refresh_row(row)
return self.redirect(self.request.route_url('mobile.{}.view'.format(self.get_route_prefix()), uuid=row.batch_uuid))
return self.render_to_response('view_row', context, mobile=True)
def attach_credit(self, row, credit_type, cases, units, expiration_date=None, discarded=None, mispick_product=None):
batch = row.batch
credit = model.PurchaseBatchCredit()
credit.credit_type = credit_type
credit.store = batch.store
credit.vendor = batch.vendor
credit.date_ordered = batch.date_ordered
credit.date_shipped = batch.date_shipped
credit.date_received = batch.date_received
credit.invoice_number = batch.invoice_number
credit.invoice_date = batch.invoice_date
credit.product = row.product
credit.upc = row.upc
credit.brand_name = row.brand_name
credit.description = row.description
credit.size = row.size
credit.department_number = row.department_number
credit.department_name = row.department_name
credit.case_quantity = row.case_quantity
credit.cases_shorted = cases
credit.units_shorted = units
credit.invoice_line_number = row.invoice_line_number
credit.invoice_case_cost = row.invoice_case_cost
credit.invoice_unit_cost = row.invoice_unit_cost
credit.invoice_total = row.invoice_total
credit.product_discarded = discarded
if credit_type == 'expired':
credit.expiration_date = expiration_date
elif credit_type == 'mispick' and mispick_product:
credit.mispick_product = mispick_product
credit.mispick_upc = mispick_product.upc
if mispick_product.brand:
credit.mispick_brand_name = mispick_product.brand.name
credit.mispick_description = mispick_product.description
credit.mispick_size = mispick_product.size
row.credits.append(credit)
return credit
@classmethod
def defaults(cls, config):
route_prefix = cls.get_route_prefix()
url_prefix = cls.get_url_prefix()
model_key = cls.get_model_key()
row_permission_prefix = cls.get_row_permission_prefix()
# mobile lookup
config.add_route('mobile.{}.lookup'.format(route_prefix), '/mobile{}/{{{}}}/lookup'.format(url_prefix, model_key))
config.add_view(cls, attr='mobile_lookup', route_name='mobile.{}.lookup'.format(route_prefix),
renderer='json', permission='{}.view'.format(row_permission_prefix))
cls._purchasing_defaults(config)
cls._batch_defaults(config)
cls._defaults(config)
class ReceivingBatchRenderer(fa.FieldRenderer):
def render_readonly(self, **kwargs):
batch = self.raw_value
title = "({}) {} for ${:0,.2f} - {}, {}".format(
batch.id_str,
batch.vendor,
batch.po_total or 0,
batch.department,
batch.created_by)
url = self.request.route_url('mobile.receiving.view', uuid=batch.uuid)
return tags.link_to(title, url)
class ValidBatchRow(forms.validators.ModelValidator):
model_class = model.PurchaseBatchRow
def _to_python(self, value, state):
row = super(ValidBatchRow, self)._to_python(value, state)
if row.batch.executed:
raise fe.Invalid("Batch has already been executed", value, state)
return row
class ReceivingForm(forms.Schema):
allow_extra_fields = True
filter_extra_fields = True
row = ValidBatchRow()
mode = fe.validators.OneOf(['received', 'damaged', 'expired',
# 'mispick',
])
# product = forms.validators.ValidProduct()
# upc = forms.validators.ValidGPC()
# brand_name = fe.validators.String()
# description = fe.validators.String()
# size = fe.validators.String()
# case_quantity = fe.validators.Number()
cases = fe.validators.Number()
units = fe.validators.Number()
# expiration_date = fe.validators.DateValidator()
# trash = fe.validators.Bool()
# ordered_product = forms.validators.ValidProduct()
def includeme(config):
ReceivingBatchView.defaults(config)