Add initial support for mobile receiving views
This commit is contained in:
parent
d68bf6b012
commit
5eca2347d5
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
4
tailbone/templates/mobile/master/view_row.mako
Normal file
4
tailbone/templates/mobile/master/view_row.mako
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
## -*- coding: utf-8; -*-
|
||||||
|
<%inherit file="/mobile/master/view.mako" />
|
||||||
|
|
||||||
|
${parent.body()}
|
48
tailbone/templates/mobile/receiving/create.mako
Normal file
48
tailbone/templates/mobile/receiving/create.mako
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
## -*- coding: utf-8; -*-
|
||||||
|
<%inherit file="/mobile/base.mako" />
|
||||||
|
|
||||||
|
<%def name="title()">${h.link_to("Receiving", url('mobile.receiving'))} » 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()}
|
18
tailbone/templates/mobile/receiving/index.mako
Normal file
18
tailbone/templates/mobile/receiving/index.mako
Normal 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()}
|
25
tailbone/templates/mobile/receiving/view.mako
Normal file
25
tailbone/templates/mobile/receiving/view.mako
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
## -*- coding: utf-8; -*-
|
||||||
|
<%inherit file="/mobile/master/view.mako" />
|
||||||
|
|
||||||
|
<%def name="title()">${h.link_to("Receiving", url('mobile.receiving'))} » ${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>
|
107
tailbone/templates/mobile/receiving/view_row.mako
Normal file
107
tailbone/templates/mobile/receiving/view_row.mako
Normal 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'))} » ${h.link_to(instance.batch.id_str, url('mobile.receiving.view', uuid=instance.batch_uuid))} » ${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"> </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()}
|
|
@ -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:
|
||||||
|
if cls.rows_viewable:
|
||||||
config.add_route('{}.view'.format(row_route_prefix), '{}/{{uuid}}'.format(row_url_prefix))
|
config.add_route('{}.view'.format(row_route_prefix), '{}/{{uuid}}'.format(row_url_prefix))
|
||||||
config.add_view(cls, attr='view_row', route_name='{}.view'.format(row_route_prefix),
|
config.add_view(cls, attr='view_row', route_name='{}.view'.format(row_route_prefix),
|
||||||
permission='{}.view'.format(permission_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:
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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",
|
||||||
|
|
322
tailbone/views/purchasing/receiving.py
Normal file
322
tailbone/views/purchasing/receiving.py
Normal 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)
|
Loading…
Reference in a new issue