Purge things for legacy (jquery) mobile, and unused template themes
gosh it feels good to get rid of this stuff... fingers crossed that nothing was broken, but am thinking it's safe
This commit is contained in:
parent
fac00e6ecd
commit
708641a8f1
|
@ -117,7 +117,6 @@ of course supply the web app layer.
|
|||
│ │ │ └── foobatch/
|
||||
│ │ ├── customers/
|
||||
│ │ ├── menu.mako
|
||||
│ │ ├── mobile/
|
||||
│ │ └── products/
|
||||
│ └── views/
|
||||
│ ├── __init__.py
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2020 Lance Edgar
|
||||
# Copyright © 2010-2021 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
|
@ -65,10 +65,5 @@ def global_help_url(config):
|
|||
return config.get('tailbone', 'global_help_url')
|
||||
|
||||
|
||||
def legacy_mobile_enabled(config):
|
||||
return config.getbool('tailbone', 'legacy_mobile.enabled',
|
||||
default=True)
|
||||
|
||||
|
||||
def protected_usernames(config):
|
||||
return config.getlist('tailbone', 'protected_usernames')
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2020 Lance Edgar
|
||||
# Copyright © 2010-2021 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
|
@ -339,7 +339,7 @@ class Form(object):
|
|||
auto_disable_save = True
|
||||
auto_disable_cancel = True
|
||||
|
||||
def __init__(self, fields=None, schema=None, request=None, mobile=False, readonly=False, readonly_fields=[],
|
||||
def __init__(self, fields=None, schema=None, request=None, readonly=False, readonly_fields=[],
|
||||
model_instance=None, model_class=None, appstruct=UNSPECIFIED, nodes={}, enums={}, labels={},
|
||||
assume_local_times=False, renderers=None,
|
||||
hidden={}, widgets={}, defaults={}, validators={}, required={}, helptext={}, focus_spec=None,
|
||||
|
@ -352,7 +352,6 @@ class Form(object):
|
|||
if self.fields is None and self.schema:
|
||||
self.set_fields([f.name for f in self.schema])
|
||||
self.request = request
|
||||
self.mobile = mobile
|
||||
self.readonly = readonly
|
||||
self.readonly_fields = set(readonly_fields or [])
|
||||
self.model_instance = model_instance
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2017 Lance Edgar
|
||||
# Copyright © 2010-2021 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
|
@ -28,4 +28,3 @@ from __future__ import unicode_literals, absolute_import
|
|||
|
||||
from . import filters
|
||||
from .core import Grid, GridAction
|
||||
from .mobile import MobileGrid
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2020 Lance Edgar
|
||||
# Copyright © 2010-2021 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
|
@ -67,7 +67,7 @@ class Grid(object):
|
|||
Core grid class. In sore need of documentation.
|
||||
"""
|
||||
|
||||
def __init__(self, key, data, columns=None, width='auto', request=None, mobile=False,
|
||||
def __init__(self, key, data, columns=None, width='auto', request=None,
|
||||
model_class=None, model_title=None, model_title_plural=None,
|
||||
enums={}, labels={}, assume_local_times=False, renderers={},
|
||||
extra_row_class=None, linked_columns=[], url='#',
|
||||
|
@ -84,7 +84,6 @@ class Grid(object):
|
|||
self.columns = FieldList(columns) if columns is not None else None
|
||||
self.width = width
|
||||
self.request = request
|
||||
self.mobile = mobile
|
||||
self.model_class = model_class
|
||||
if self.model_class and self.columns is None:
|
||||
self.columns = self.make_columns()
|
||||
|
@ -341,7 +340,6 @@ class Grid(object):
|
|||
def make_webhelpers_grid(self):
|
||||
kwargs = dict(self._whgrid_kwargs)
|
||||
kwargs['request'] = self.request
|
||||
kwargs['mobile'] = self.mobile
|
||||
kwargs['url'] = self.make_url
|
||||
|
||||
columns = list(self.columns)
|
||||
|
@ -1302,17 +1300,11 @@ class CustomWebhelpersGrid(webhelpers2_grid.Grid):
|
|||
"""
|
||||
|
||||
def __init__(self, itemlist, columns, **kwargs):
|
||||
self.mobile = kwargs.pop('mobile', False)
|
||||
self.renderers = kwargs.pop('renderers', {})
|
||||
self.linked_columns = kwargs.pop('linked_columns', [])
|
||||
self.extra_record_class = kwargs.pop('extra_record_class', None)
|
||||
super(CustomWebhelpersGrid, self).__init__(itemlist, columns, **kwargs)
|
||||
|
||||
def default_header_record_format(self, headers):
|
||||
if self.mobile:
|
||||
return HTML('')
|
||||
return super(CustomWebhelpersGrid, self).default_header_record_format(headers)
|
||||
|
||||
def generate_header_link(self, column_number, column, label_text):
|
||||
|
||||
# display column header as simple no-op link; client-side JS takes care
|
||||
|
@ -1329,8 +1321,6 @@ class CustomWebhelpersGrid(webhelpers2_grid.Grid):
|
|||
label_text)
|
||||
|
||||
def default_record_format(self, i, record, columns):
|
||||
if self.mobile:
|
||||
return columns
|
||||
kwargs = {
|
||||
'class_': self.get_record_class(i, record, columns),
|
||||
}
|
||||
|
@ -1359,12 +1349,6 @@ class CustomWebhelpersGrid(webhelpers2_grid.Grid):
|
|||
|
||||
def default_column_format(self, column_number, i, record, column_name):
|
||||
value = self.get_column_value(column_number, i, record, column_name)
|
||||
if self.mobile:
|
||||
url = self.url_generator(record, i)
|
||||
attrs = {}
|
||||
if hasattr(record, 'uuid'):
|
||||
attrs['data_uuid'] = record.uuid
|
||||
return HTML.tag('li', tags.link_to(value, url), **attrs)
|
||||
if self.linked_columns and column_name in self.linked_columns and (
|
||||
value is not None and value != ''):
|
||||
url = self.url_generator(record, i)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2020 Lance Edgar
|
||||
# Copyright © 2010-2021 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
|
@ -294,18 +294,6 @@ class GridFilter(object):
|
|||
return self.value_renderer.render(value=value, **kwargs)
|
||||
|
||||
|
||||
class MobileFilter(GridFilter):
|
||||
"""
|
||||
Base class for mobile grid filters.
|
||||
"""
|
||||
default_verbs = ['equal']
|
||||
|
||||
def __init__(self, key, **kwargs):
|
||||
kwargs.setdefault('default_active', True)
|
||||
kwargs.setdefault('default_verb', 'equal')
|
||||
super(MobileFilter, self).__init__(key, **kwargs)
|
||||
|
||||
|
||||
class AlchemyGridFilter(GridFilter):
|
||||
"""
|
||||
Base class for SQLAlchemy grid filters.
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
# -*- 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 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Mobile Grids
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals, absolute_import
|
||||
|
||||
from pyramid.renderers import render
|
||||
|
||||
from .core import Grid
|
||||
|
||||
|
||||
class MobileGrid(Grid):
|
||||
"""
|
||||
Base class for all mobile grids
|
||||
"""
|
||||
|
||||
def render_filters(self, template='/mobile/grids/filters_simple.mako', **kwargs):
|
||||
context = kwargs
|
||||
context['request'] = self.request
|
||||
context['grid'] = self
|
||||
return render(template, context)
|
||||
|
||||
def render_grid(self, template='/mobile/grids/grid.mako', **kwargs):
|
||||
context = kwargs
|
||||
context['grid'] = self
|
||||
return render(template, context)
|
||||
|
||||
def render_complete(self, template='/mobile/grids/complete.mako', **kwargs):
|
||||
context = kwargs
|
||||
context['grid'] = self
|
||||
return render(template, context)
|
|
@ -1,57 +0,0 @@
|
|||
|
||||
/****************************************
|
||||
* Global styles for mobile templates
|
||||
****************************************/
|
||||
|
||||
/* main user menu button when root */
|
||||
[data-role="header"] a.root-user,
|
||||
[data-role="header"] a.root-user:hover {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
/* become/stop root menu links */
|
||||
#usermenu .root-user a {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
/* normal flash messages */
|
||||
.flash {
|
||||
color: green;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
/* error flash messages */
|
||||
.error,
|
||||
.error-messages {
|
||||
color: red;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
/* receiving warning flash messages */
|
||||
.receiving-warning {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.replacement-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.field-wrapper.with-error {
|
||||
background-color: #ddcccc;
|
||||
border: 2px solid #dd6666;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.field-wrapper label {
|
||||
font-weight: bold;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.field-error .error-msg {
|
||||
color: Red;
|
||||
}
|
||||
|
||||
/* make sure space comes between simple filter and "grid" list */
|
||||
.simple-filter {
|
||||
margin-bottom: 1.5em;
|
||||
}
|
|
@ -1,81 +0,0 @@
|
|||
|
||||
/******************************************
|
||||
* jQuery Mobile plugins for Tailbone
|
||||
*****************************************/
|
||||
|
||||
/******************************************
|
||||
* mobile autocomplete
|
||||
*****************************************/
|
||||
|
||||
(function($) {
|
||||
|
||||
$.widget('tailbone.mobileautocomplete', {
|
||||
|
||||
_create: function() {
|
||||
var that = this;
|
||||
|
||||
// snag some element references
|
||||
this.search = this.element.find('.ui-input-search');
|
||||
this.hidden_field = this.element.find('input[type="hidden"]');
|
||||
this.text_field = this.element.find('input[type="text"]');
|
||||
this.ul = this.element.find('ul');
|
||||
this.button = this.element.find('button');
|
||||
|
||||
// establish our autocomplete URL
|
||||
this.url = this.options.url || this.element.data('url');
|
||||
|
||||
// NOTE: much of this code was copied from the jquery mobile demo site
|
||||
// https://demos.jquerymobile.com/1.4.5/listview-autocomplete-remote/
|
||||
this.ul.on('filterablebeforefilter', function(e, data) {
|
||||
|
||||
var $input = $( data.input ),
|
||||
value = $input.val(),
|
||||
html = "";
|
||||
that.ul.html( "" );
|
||||
if ( value && value.length > 2 ) {
|
||||
that.ul.html( "<li><div class='ui-loader'><span class='ui-icon ui-icon-loading'></span></div></li>" );
|
||||
that.ul.listview( "refresh" );
|
||||
$.ajax({
|
||||
url: that.url,
|
||||
data: {
|
||||
term: $input.val()
|
||||
}
|
||||
})
|
||||
.then( function ( response ) {
|
||||
$.each( response, function ( i, val ) {
|
||||
html += '<li data-uuid="' + val.value + '">' + val.label + "</li>";
|
||||
});
|
||||
that.ul.html( html );
|
||||
that.ul.listview( "refresh" );
|
||||
that.ul.trigger( "updatelayout");
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// when user clicks autocomplete result, hide search etc.
|
||||
this.ul.on('click', 'li', function() {
|
||||
var $li = $(this);
|
||||
var uuid = $li.data('uuid');
|
||||
that.search.hide();
|
||||
that.hidden_field.val(uuid);
|
||||
that.button.text($li.text()).show();
|
||||
that.ul.hide();
|
||||
that.element.trigger('autocompleteitemselected', uuid);
|
||||
});
|
||||
|
||||
// when user clicks "change" button, show search etc.
|
||||
this.button.click(function() {
|
||||
that.button.hide();
|
||||
that.ul.empty().show();
|
||||
that.hidden_field.val('');
|
||||
that.search.show();
|
||||
that.text_field.focus();
|
||||
that.element.trigger('autocompleteitemcleared');
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
})( jQuery );
|
|
@ -1,308 +0,0 @@
|
|||
|
||||
/************************************************************
|
||||
*
|
||||
* tailbone.mobile.js
|
||||
*
|
||||
* Global logic for mobile app
|
||||
*
|
||||
************************************************************/
|
||||
|
||||
|
||||
$(function() {
|
||||
|
||||
// must init header/footer toolbars since ours are "external"
|
||||
$('[data-role="header"], [data-role="footer"]').toolbar({theme: 'a'});
|
||||
});
|
||||
|
||||
|
||||
$(document).on('pagecontainerchange', function(event, ui) {
|
||||
|
||||
// in some cases (i.e. when no user is logged in) we may want the (external)
|
||||
// header toolbar button to change between pages. here's how we do that.
|
||||
// note however that we do this *always* even when not technically needed
|
||||
var link = $('[data-role="header"] a:first');
|
||||
var newlink = ui.toPage.find('.replacement-header a');
|
||||
link.text(newlink.text());
|
||||
link.attr('href', newlink.attr('href'));
|
||||
link.removeClass('ui-icon-home ui-icon-user');
|
||||
link.addClass(newlink.attr('class'));
|
||||
});
|
||||
|
||||
|
||||
$(document).on('click', '#feedback-button', function() {
|
||||
|
||||
// prepare and display 'feedback' popup dialog
|
||||
var popup = $('.ui-page-active #feedback-popup');
|
||||
popup.find('.referrer .field').html(location.href);
|
||||
popup.find('.referrer input').val(location.href);
|
||||
popup.find('.user_name input').val('');
|
||||
popup.find('.message textarea').val('');
|
||||
popup.data('feedback-sent', false);
|
||||
popup.popup('open');
|
||||
});
|
||||
|
||||
|
||||
$(document).on('click', '#feedback-popup .submit', function() {
|
||||
|
||||
// send message when 'feedback' submit button pressed
|
||||
var popup = $('.ui-page-active #feedback-popup');
|
||||
var form = popup.find('form');
|
||||
$.post(form.attr('action'), form.serializeArray(), function(data) {
|
||||
if (data.ok) {
|
||||
|
||||
// mark "feedback sent" flag, for popupafterclose
|
||||
popup.data('feedback-sent', true);
|
||||
popup.popup('close');
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
$(document).on('click', '#feedback-form-buttons .cancel', function() {
|
||||
|
||||
// close 'feedback' popup when user clicks Cancel
|
||||
var popup = $('.ui-page-active #feedback-popup');
|
||||
popup.popup('close');
|
||||
});
|
||||
|
||||
|
||||
$(document).on('popupafterclose', '#feedback-popup', function() {
|
||||
|
||||
// thank the user for their feedback, after msg is sent
|
||||
if ($(this).data('feedback-sent')) {
|
||||
var popup = $('.ui-page-active #feedback-thanks');
|
||||
popup.popup('open');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
$(document).on('pagecreate', function() {
|
||||
|
||||
// setup any autocomplete fields
|
||||
$('.field.autocomplete').mobileautocomplete();
|
||||
|
||||
});
|
||||
|
||||
|
||||
// submit "quick row" form upon autocomplete selection
|
||||
$(document).on('autocompleteitemselected', function(event, uuid) {
|
||||
var field = $(event.target);
|
||||
if (field.hasClass('quick-row')) {
|
||||
var form = field.parents('form:first');
|
||||
form.find('[name="quick_entry"]').val(uuid);
|
||||
form.submit();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Automatically set focus to certain fields, on various pages
|
||||
* TODO: should be letting the form declare a "focus spec" instead, to avoid
|
||||
* hard-coding these field names below!
|
||||
*/
|
||||
function setfocus() {
|
||||
var el = null;
|
||||
var queries = [
|
||||
'#username',
|
||||
'#new-purchasing-batch-vendor-text',
|
||||
'#new-receiving-batch-vendor-text',
|
||||
];
|
||||
$.each(queries, function(i, query) {
|
||||
el = $(query);
|
||||
if (el.is(':visible')) {
|
||||
el.focus();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
$(document).on('pageshow', function() {
|
||||
|
||||
setfocus();
|
||||
|
||||
// if current page has form, which has declared a "focus spec", then try to
|
||||
// set focus accordingly
|
||||
var form = $('.ui-page-active form');
|
||||
if (form) {
|
||||
var spec = form.data('focus');
|
||||
if (spec) {
|
||||
var input = $(spec);
|
||||
if (input) {
|
||||
if (input.is(':visible')) {
|
||||
input.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
// handle radio button value change for "simple" grid filter
|
||||
$(document).on('change', '.simple-filter .ui-radio', function() {
|
||||
$(this).parents('form:first').submit();
|
||||
});
|
||||
|
||||
|
||||
// vendor validation for new purchasing batch
|
||||
$(document).on('click', 'form[name="new-purchasing-batch"] input[type="submit"]', function() {
|
||||
var $form = $(this).parents('form');
|
||||
if (! $form.find('[name="vendor"]').val()) {
|
||||
alert("Please select a vendor");
|
||||
$form.find('[name="new-purchasing-batch-vendor-text"]').focus();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// disable datasync restart button when clicked
|
||||
$(document).on('click', '#datasync-restart', function() {
|
||||
$(this).button('disable');
|
||||
});
|
||||
|
||||
|
||||
// TODO: this should go away in favor of quick_row approach
|
||||
// handle global keypress on product batch "row" page, for sake of scanner wedge
|
||||
var product_batch_routes = [
|
||||
'mobile.batch.inventory.view',
|
||||
];
|
||||
$(document).on('keypress', function(event) {
|
||||
var current_route = $('.ui-page-active [role="main"]').data('route');
|
||||
for (var route of product_batch_routes) {
|
||||
if (current_route == route) {
|
||||
var upc = $('.ui-page-active #upc-search');
|
||||
if (upc.length) {
|
||||
if (upc.is(':focus')) {
|
||||
if (event.which == 13) {
|
||||
if (upc.val()) {
|
||||
$.mobile.navigate(upc.data('url') + '?upc=' + upc.val());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (event.which >= 48 && event.which <= 57) { // numeric (qwerty)
|
||||
upc.val(upc.val() + event.key);
|
||||
// TODO: these codes are correct for 'keydown' but apparently not 'keypress' ?
|
||||
// } else if (event.which >= 96 && event.which <= 105) { // numeric (10-key)
|
||||
// upc.val(upc.val() + event.key);
|
||||
} else if (event.which == 13) {
|
||||
if (upc.val()) {
|
||||
$.mobile.navigate(upc.data('url') + '?upc=' + upc.val());
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// handle various keypress events for quick entry forms
|
||||
$(document).on('keypress', function(event) {
|
||||
var quick_entry = $('.ui-page-active #quick_entry');
|
||||
if (quick_entry.length) {
|
||||
|
||||
// if user hits enter with quick row input focused, submit form
|
||||
if (quick_entry.is(':focus')) {
|
||||
if (event.which == 13) { // ENTER
|
||||
if (quick_entry.val()) {
|
||||
var form = quick_entry.parents('form:first');
|
||||
form.submit();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
} else { // quick row input not focused
|
||||
|
||||
// mimic keyboard wedge if we're so instructed
|
||||
if (quick_entry.data('wedge')) {
|
||||
|
||||
if (event.which >= 48 && event.which <= 57) { // numeric (qwerty)
|
||||
if (!event.altKey && !event.ctrlKey && !event.metaKey) {
|
||||
quick_entry.val(quick_entry.val() + event.key);
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: these codes are correct for 'keydown' but apparently not 'keypress' ?
|
||||
// } else if (event.which >= 96 && event.which <= 105) { // numeric (10-key)
|
||||
// upc.val(upc.val() + event.key);
|
||||
|
||||
} else if (event.which == 13) { // ENTER
|
||||
// submit form when ENTER is received via keyboard "wedge"
|
||||
if (quick_entry.val()) {
|
||||
var form = quick_entry.parents('form:first');
|
||||
form.submit();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// when numeric keypad button is clicked, update quantity accordingly
|
||||
$(document).on('click', '.quantity-keypad-thingy .keypad-button', function() {
|
||||
var keypad = $(this).parents('.quantity-keypad-thingy');
|
||||
var quantity = keypad.find('.keypad-quantity');
|
||||
var value = quantity.text();
|
||||
var key = $(this).text();
|
||||
var changed = keypad.data('changed');
|
||||
if (key == 'Del') {
|
||||
if (value.length == 1) {
|
||||
quantity.text('0');
|
||||
} else {
|
||||
quantity.text(value.substring(0, value.length - 1));
|
||||
}
|
||||
changed = true;
|
||||
} else if (key == '.') {
|
||||
if (value.indexOf('.') == -1) {
|
||||
if (changed) {
|
||||
quantity.text(value + '.');
|
||||
} else {
|
||||
quantity.text('0.');
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (value == '0') {
|
||||
quantity.text(key);
|
||||
changed = true;
|
||||
} else if (changed) {
|
||||
quantity.text(value + key);
|
||||
} else {
|
||||
quantity.text(key);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
keypad.data('changed', true);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// show/hide expiration date per receiving mode selection
|
||||
$(document).on('change', 'fieldset.receiving-mode input[name="mode"]', function() {
|
||||
var mode = $(this).val();
|
||||
if (mode == 'expired') {
|
||||
$('#expiration-row').show();
|
||||
} else {
|
||||
$('#expiration-row').hide();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// handle inventory save button
|
||||
$(document).on('click', '.inventory-actions button.save', function() {
|
||||
var form = $(this).parents('form:first');
|
||||
var uom = form.find('[name="keypad-uom"]:checked').val();
|
||||
var qty = form.find('.keypad-quantity').text();
|
||||
if (uom == 'CS') {
|
||||
form.find('input[name="cases"]').val(qty);
|
||||
} else { // units
|
||||
form.find('input[name="units"]').val(qty);
|
||||
}
|
||||
form.submit();
|
||||
});
|
|
@ -1,92 +0,0 @@
|
|||
|
||||
/************************************************************
|
||||
*
|
||||
* tailbone.mobile.receiving.js
|
||||
*
|
||||
* Global logic for mobile receiving feature
|
||||
*
|
||||
************************************************************/
|
||||
|
||||
|
||||
// toggle visibility of "Receive" type buttons based on whether vendor is set
|
||||
$(document).on('autocompleteitemselected', 'form[name="new-receiving-batch"] .vendor', function(event, uuid) {
|
||||
$('#new-receiving-types').show();
|
||||
});
|
||||
$(document).on('autocompleteitemcleared', 'form[name="new-receiving-batch"] .vendor', function(event) {
|
||||
$('#new-receiving-types').hide();
|
||||
});
|
||||
$(document).on('change', 'form[name="new-receiving-batch"] select[name="vendor"]', function(event) {
|
||||
if ($(this).val()) {
|
||||
$('#new-receiving-types').show();
|
||||
} else {
|
||||
$('#new-receiving-types').hide();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// submit new receiving batch form when user clicks "Receive" type button
|
||||
$(document).on('click', 'form[name="new-receiving-batch"] .start-receiving', function() {
|
||||
var form = $(this).parents('form');
|
||||
form.find('input[name="workflow"]').val($(this).data('workflow'));
|
||||
form.submit();
|
||||
});
|
||||
|
||||
|
||||
// submit new receiving batch form when user clicks Purchase Order option
|
||||
$(document).on('click', 'form[name="new-receiving-batch"] [data-role="listview"] a', function() {
|
||||
var form = $(this).parents('form');
|
||||
var key = $(this).parents('li').data('key');
|
||||
form.find('[name="workflow"]').val('from_po');
|
||||
form.find('.purchase-order-field').val(key);
|
||||
form.submit();
|
||||
return false;
|
||||
});
|
||||
|
||||
|
||||
// handle receiving action buttons
|
||||
$(document).on('click', 'form.receiving-update .receiving-actions button', function() {
|
||||
var action = $(this).data('action');
|
||||
var form = $(this).parents('form:first');
|
||||
var uom = form.find('[name="keypad-uom"]:checked').val();
|
||||
var mode = form.find('[name="mode"]:checked').val();
|
||||
var qty = form.find('.keypad-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);
|
||||
}
|
||||
|
||||
if (action == 'add' && mode == 'expired') {
|
||||
var expiry = form.find('input[name="expiration_date"]');
|
||||
if (! /^\d{4}-\d{2}-\d{2}$/.test(expiry.val())) {
|
||||
alert("Please enter a valid expiration date.");
|
||||
expiry.focus();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// quick-receive (1 case or unit)
|
||||
$(document).on('click', 'form.receiving-update .quick-receive', function() {
|
||||
var form = $(this).parents('form:first');
|
||||
form.find('[name="mode"]').val('received');
|
||||
var quantity = $(this).data('quantity');
|
||||
if ($(this).data('uom') == 'CS') {
|
||||
form.find('[name="cases"]').val(quantity);
|
||||
} else {
|
||||
form.find('[name="units"]').val(quantity);
|
||||
}
|
||||
form.find('input[name="quick_receive"]').val('true');
|
||||
form.submit();
|
||||
});
|
|
@ -1,114 +0,0 @@
|
|||
|
||||
/* /\****************************** */
|
||||
/* * General */
|
||||
/* ******************************\/ */
|
||||
|
||||
/* * { */
|
||||
/* margin: 0px; */
|
||||
/* } */
|
||||
|
||||
/* body { */
|
||||
/* font-family: Verdana, Arial, sans-serif; */
|
||||
/* font-size: 11pt; */
|
||||
/* } */
|
||||
|
||||
/* a { */
|
||||
/* color: #0972a5; */
|
||||
/* text-decoration: none; */
|
||||
/* } */
|
||||
|
||||
/* a:hover { */
|
||||
/* text-decoration: underline; */
|
||||
/* } */
|
||||
|
||||
/* h1 { */
|
||||
/* margin-bottom: 15px; */
|
||||
/* } */
|
||||
|
||||
/* h2 { */
|
||||
/* font-size: 12pt; */
|
||||
/* margin: 20px auto 10px auto; */
|
||||
/* } */
|
||||
|
||||
/* li { */
|
||||
/* line-height: 2em; */
|
||||
/* } */
|
||||
|
||||
/* p { */
|
||||
/* margin-bottom: 5px; */
|
||||
/* } */
|
||||
|
||||
/* .left { */
|
||||
/* float: left; */
|
||||
/* text-align: left; */
|
||||
/* } */
|
||||
|
||||
/* .right { */
|
||||
/* text-align: right; */
|
||||
/* } */
|
||||
|
||||
/* .wrapper { */
|
||||
/* overflow: auto; */
|
||||
/* } */
|
||||
|
||||
/* div.buttons { */
|
||||
/* clear: both; */
|
||||
/* margin-top: 10px; */
|
||||
/* } */
|
||||
|
||||
/* div.dialog { */
|
||||
/* display: none; */
|
||||
/* } */
|
||||
|
||||
/* div.flash-message { */
|
||||
/* background-color: #dddddd; */
|
||||
/* margin-bottom: 8px; */
|
||||
/* padding: 3px; */
|
||||
/* } */
|
||||
|
||||
/* div.flash-messages div.ui-state-highlight { */
|
||||
/* padding: .3em; */
|
||||
/* margin-bottom: 8px; */
|
||||
/* } */
|
||||
|
||||
/* div.error-messages div.ui-state-error { */
|
||||
/* padding: .3em; */
|
||||
/* margin-bottom: 8px; */
|
||||
/* } */
|
||||
|
||||
/* .flash-messages, */
|
||||
/* .error-messages { */
|
||||
/* margin: 0.5em 0 0 0; */
|
||||
/* } */
|
||||
|
||||
/* ul.error { */
|
||||
/* color: #dd6666; */
|
||||
/* font-weight: bold; */
|
||||
/* padding: 0px; */
|
||||
/* } */
|
||||
|
||||
/* ul.error li { */
|
||||
/* list-style-type: none; */
|
||||
/* } */
|
||||
|
||||
/* /\****************************** */
|
||||
/* * jQuery UI tweaks */
|
||||
/* ******************************\/ */
|
||||
|
||||
/* ul.ui-menu { */
|
||||
/* max-height: 30em; */
|
||||
/* } */
|
||||
|
||||
/******************************
|
||||
* tweaks for root user
|
||||
******************************/
|
||||
|
||||
.navbar .navbar-end .navbar-link.root-user,
|
||||
.navbar .navbar-end .navbar-link.root-user:hover,
|
||||
.navbar .navbar-end .navbar-link.root-user.is_active,
|
||||
.navbar .navbar-end .navbar-item.root-user,
|
||||
.navbar .navbar-end .navbar-item.root-user:hover,
|
||||
.navbar .navbar-end .navbar-item.root-user.is_active {
|
||||
background-color: red;
|
||||
font-weight: bold;
|
||||
}
|
|
@ -1,141 +0,0 @@
|
|||
|
||||
/* /\****************************** */
|
||||
/* * Form Wrapper */
|
||||
/* ******************************\/ */
|
||||
|
||||
/* div.form-wrapper { */
|
||||
/* overflow: auto; */
|
||||
/* } */
|
||||
|
||||
|
||||
/******************************
|
||||
* context menu
|
||||
******************************/
|
||||
|
||||
/* #context-menu { */
|
||||
/* /\* background-color: #ddcccc; *\/ */
|
||||
/* /\* background-color: green; *\/ */
|
||||
/* float: right; */
|
||||
/* /\* list-style-type: none; *\/ */
|
||||
/* /\* margin: 0px; *\/ */
|
||||
/* text-align: right; */
|
||||
/* } */
|
||||
|
||||
/* div.form-wrapper ul.context-menu li { */
|
||||
/* line-height: 2em; */
|
||||
/* } */
|
||||
|
||||
|
||||
/* /\****************************** */
|
||||
/* * "object helper" panel */
|
||||
/* ******************************\/ */
|
||||
|
||||
/* .object-helper { */
|
||||
/* border: 1px solid black; */
|
||||
/* float: right; */
|
||||
/* margin-top: 1em; */
|
||||
/* padding: 1em; */
|
||||
/* width: 20em; */
|
||||
/* } */
|
||||
|
||||
/* .object-helper-content { */
|
||||
/* margin-top: 1em; */
|
||||
/* } */
|
||||
|
||||
|
||||
/******************************
|
||||
* forms
|
||||
******************************/
|
||||
|
||||
/* div.form, */
|
||||
/* div.fieldset-form, */
|
||||
/* div.fieldset { */
|
||||
/* clear: left; */
|
||||
/* float: left; */
|
||||
/* margin-top: 10px; */
|
||||
/* } */
|
||||
|
||||
/* TODO: replace this with bulma equivalent */
|
||||
.form {
|
||||
padding-left: 5em;
|
||||
}
|
||||
|
||||
|
||||
/******************************
|
||||
* fieldsets
|
||||
******************************/
|
||||
|
||||
/* TODO: replace this with bulma equivalent */
|
||||
.field-wrapper {
|
||||
clear: both;
|
||||
min-height: 30px;
|
||||
overflow: auto;
|
||||
margin: 15px;
|
||||
}
|
||||
|
||||
/* .field-wrapper.with-error { */
|
||||
/* background-color: #ddcccc; */
|
||||
/* border: 2px solid #dd6666; */
|
||||
/* padding-bottom: 1em; */
|
||||
/* } */
|
||||
|
||||
/* TODO: replace this with bulma equivalent */
|
||||
.field-wrapper .field-row {
|
||||
display: table-row;
|
||||
}
|
||||
|
||||
/* TODO: replace this with bulma equivalent */
|
||||
.field-wrapper label {
|
||||
display: table-cell;
|
||||
vertical-align: top;
|
||||
width: 18em;
|
||||
font-weight: bold;
|
||||
padding-top: 2px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* .field-wrapper.with-error label { */
|
||||
/* padding-left: 1em; */
|
||||
/* } */
|
||||
|
||||
/* .field-wrapper .field-error { */
|
||||
/* padding: 1em 0 0.5em 1em; */
|
||||
/* } */
|
||||
|
||||
/* .field-wrapper .field-error .error-msg { */
|
||||
/* color: #dd6666; */
|
||||
/* font-weight: bold; */
|
||||
/* } */
|
||||
|
||||
/* TODO: replace this with bulma equivalent */
|
||||
.field-wrapper .field {
|
||||
display: table-cell;
|
||||
line-height: 25px;
|
||||
}
|
||||
|
||||
/* .field-wrapper .field input[type=text], */
|
||||
/* .field-wrapper .field input[type=password], */
|
||||
/* .field-wrapper .field select, */
|
||||
/* .field-wrapper .field textarea { */
|
||||
/* width: 320px; */
|
||||
/* } */
|
||||
|
||||
/* label input[type="checkbox"], */
|
||||
/* label input[type="radio"] { */
|
||||
/* margin-right: 0.5em; */
|
||||
/* } */
|
||||
|
||||
/* .field ul { */
|
||||
/* margin: 0px; */
|
||||
/* padding-left: 15px; */
|
||||
/* } */
|
||||
|
||||
|
||||
/* /\****************************** */
|
||||
/* * Buttons */
|
||||
/* ******************************\/ */
|
||||
|
||||
/* div.buttons { */
|
||||
/* clear: both; */
|
||||
/* margin: 10px 0px; */
|
||||
/* } */
|
|
@ -1,208 +0,0 @@
|
|||
|
||||
/******************************
|
||||
* main layout
|
||||
******************************/
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
|
||||
/******************************
|
||||
* header
|
||||
******************************/
|
||||
|
||||
header .level {
|
||||
/* height: 60px; */
|
||||
line-height: 60px;
|
||||
padding-left: 0.5em;
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
|
||||
header .level #header-logo {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
header .level .global-title,
|
||||
header .level-left .global-title {
|
||||
font-size: 2em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
header .level #current-context,
|
||||
header .level-left #current-context {
|
||||
font-size: 2em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
header .level #current-context span,
|
||||
header .level-left #current-context span {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
header .level .theme-picker {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
/* header .global .grid-nav { */
|
||||
/* display: inline-block; */
|
||||
/* font-size: 16px; */
|
||||
/* font-weight: bold; */
|
||||
/* line-height: 60px; */
|
||||
/* margin-left: 5em; */
|
||||
/* } */
|
||||
|
||||
/* header .global .grid-nav .ui-button, */
|
||||
/* header .global .grid-nav span.viewing { */
|
||||
/* margin-left: 1em; */
|
||||
/* } */
|
||||
|
||||
#content-title h1 {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
/* /\****************************** */
|
||||
/* * Logo */
|
||||
/* ******************************\/ */
|
||||
|
||||
/* #logo { */
|
||||
/* display: block; */
|
||||
/* margin: 40px auto; */
|
||||
/* } */
|
||||
|
||||
|
||||
/******************************
|
||||
* content
|
||||
******************************/
|
||||
|
||||
#page-body {
|
||||
padding: 0.4em;
|
||||
}
|
||||
|
||||
/* body > #body-wrapper { */
|
||||
/* margin: 0px; */
|
||||
/* position: relative; */
|
||||
/* } */
|
||||
|
||||
/* .content-wrapper { */
|
||||
/* height: 100%; */
|
||||
/* padding-bottom: 30px; */
|
||||
/* } */
|
||||
|
||||
/* #scrollpane { */
|
||||
/* height: 100%; */
|
||||
/* } */
|
||||
|
||||
/* #scrollpane .inner-content { */
|
||||
/* padding: 0 0.5em 0.5em 0.5em; */
|
||||
/* } */
|
||||
|
||||
|
||||
/******************************
|
||||
* context menu
|
||||
******************************/
|
||||
|
||||
#context-menu {
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/******************************
|
||||
* "object helper" panel
|
||||
******************************/
|
||||
|
||||
.object-helper {
|
||||
border: 1px solid black;
|
||||
margin: 1em;
|
||||
padding: 1em;
|
||||
min-width: 20em;
|
||||
}
|
||||
|
||||
.object-helper-content {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
/* /\****************************** */
|
||||
/* * Panels */
|
||||
/* ******************************\/ */
|
||||
|
||||
/* .panel-wrapper { */
|
||||
/* float: left; */
|
||||
/* margin-right: 15px; */
|
||||
/* width: 40%; */
|
||||
/* } */
|
||||
|
||||
/* .panel, */
|
||||
/* .panel-grid { */
|
||||
/* border-left: 1px solid Black; */
|
||||
/* margin-bottom: 15px; */
|
||||
/* } */
|
||||
|
||||
/* .panel { */
|
||||
/* border-bottom: 1px solid Black; */
|
||||
/* border-right: 1px solid Black; */
|
||||
/* padding: 0px; */
|
||||
/* } */
|
||||
|
||||
/* .panel h2, */
|
||||
/* .panel-grid h2 { */
|
||||
/* border-bottom: 1px solid Black; */
|
||||
/* border-top: 1px solid Black; */
|
||||
/* padding: 5px; */
|
||||
/* margin: 0px; */
|
||||
/* } */
|
||||
|
||||
/* .panel-grid h2 { */
|
||||
/* border-right: 1px solid Black; */
|
||||
/* } */
|
||||
|
||||
/* .panel-body { */
|
||||
/* overflow: auto; */
|
||||
/* padding: 5px; */
|
||||
/* } */
|
||||
|
||||
/******************************
|
||||
* feedback
|
||||
******************************/
|
||||
|
||||
#feedback-dialog {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#feedback-dialog p {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
#feedback-dialog .red {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#feedback-dialog .field-wrapper {
|
||||
margin-top: 1em;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#feedback-dialog .field {
|
||||
margin-bottom: 0;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
#feedback-dialog .referrer .field {
|
||||
clear: both;
|
||||
float: none;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
#feedback-dialog textarea {
|
||||
width: auto;
|
||||
}
|
|
@ -1,84 +0,0 @@
|
|||
/* copied from https://github.com/dansup/bulma-templates/blob/master/css/admin.css */
|
||||
|
||||
html, body {
|
||||
font-family: 'Open Sans', serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
height: 100%;
|
||||
background: #ECF0F3;
|
||||
}
|
||||
nav.navbar {
|
||||
border-top: 4px solid #276cda;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.navbar-item.brand-text {
|
||||
font-weight: 300;
|
||||
}
|
||||
.navbar-item, .navbar-link {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.columns {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin-left: 0;
|
||||
}
|
||||
.menu-label {
|
||||
color: #8F99A3;
|
||||
letter-spacing: 1.3;
|
||||
font-weight: 700;
|
||||
}
|
||||
.menu-list a {
|
||||
color: #0F1D38;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.menu-list a:hover {
|
||||
background-color: transparent;
|
||||
color: #276cda;
|
||||
}
|
||||
.menu-list a.is-active {
|
||||
background-color: transparent;
|
||||
color: #276cda;
|
||||
font-weight: 700;
|
||||
}
|
||||
.card {
|
||||
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.18);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.card-header-title {
|
||||
color: #8F99A3;
|
||||
font-weight: 400;
|
||||
}
|
||||
.info-tiles {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.info-tiles .subtitle {
|
||||
font-weight: 300;
|
||||
color: #8F99A3;
|
||||
}
|
||||
.hero.welcome.is-info {
|
||||
background: #36D1DC;
|
||||
background: -webkit-linear-gradient(to right, #5B86E5, #36D1DC);
|
||||
background: linear-gradient(to right, #5B86E5, #36D1DC);
|
||||
}
|
||||
.hero.welcome .title, .hero.welcome .subtitle {
|
||||
color: hsl(192, 17%, 99%);
|
||||
}
|
||||
.card .content {
|
||||
font-size: 14px;
|
||||
}
|
||||
.card-footer-item {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #8F99A3;
|
||||
}
|
||||
.card-footer-item:hover {
|
||||
}
|
||||
.card-table .table {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.events-card .card-table {
|
||||
max-height: 250px;
|
||||
overflow-y: scroll;
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
|
||||
/******************************
|
||||
* tweaks for root user
|
||||
******************************/
|
||||
|
||||
.navbar .navbar-menu .navbar-link.root-user,
|
||||
.navbar .navbar-menu .navbar-item.root-user,
|
||||
.navbar.is-white .navbar-item.has-dropdown.is-active .navbar-link.root-user,
|
||||
.navbar.is-white .navbar-item.has-dropdown:hover .navbar-link.root-user {
|
||||
background-color: red;
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
// copied from https://github.com/dansup/bulma-templates/blob/master/js/bulma.js
|
||||
|
||||
// The following code is based off a toggle menu by @Bradcomp
|
||||
// source: https://gist.github.com/Bradcomp/a9ef2ef322a8e8017443b626208999c1
|
||||
(function() {
|
||||
var burger = document.querySelector('.burger');
|
||||
var menu = document.querySelector('#'+burger.dataset.target);
|
||||
burger.addEventListener('click', function() {
|
||||
burger.classList.toggle('is-active');
|
||||
menu.classList.toggle('is-active');
|
||||
});
|
||||
})();
|
|
@ -26,3 +26,36 @@
|
|||
.form-wrapper .form .field.is-horizontal .field-body .select select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/******************************
|
||||
* field-wrappers
|
||||
******************************/
|
||||
|
||||
/* TODO: replace this with bulma equivalent */
|
||||
.field-wrapper {
|
||||
clear: both;
|
||||
min-height: 30px;
|
||||
overflow: auto;
|
||||
margin: 15px;
|
||||
}
|
||||
|
||||
/* TODO: replace this with bulma equivalent */
|
||||
.field-wrapper .field-row {
|
||||
display: table-row;
|
||||
}
|
||||
|
||||
/* TODO: replace this with bulma equivalent */
|
||||
.field-wrapper label {
|
||||
display: table-cell;
|
||||
vertical-align: top;
|
||||
width: 18em;
|
||||
font-weight: bold;
|
||||
padding-top: 2px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* TODO: replace this with bulma equivalent */
|
||||
.field-wrapper .field {
|
||||
display: table-cell;
|
||||
line-height: 25px;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2020 Lance Edgar
|
||||
# Copyright © 2010-2021 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
|
@ -208,7 +208,7 @@ def context_found(event):
|
|||
return False
|
||||
request.has_any_perm = has_any_perm
|
||||
|
||||
def get_referrer(default=None, mobile=False):
|
||||
def get_referrer(default=None, **kwargs):
|
||||
if request.params.get('referrer'):
|
||||
return request.params['referrer']
|
||||
if request.session.get('referrer'):
|
||||
|
@ -218,8 +218,6 @@ def context_found(event):
|
|||
or not referrer.startswith(request.host_url)):
|
||||
if default:
|
||||
referrer = default
|
||||
elif mobile:
|
||||
referrer = request.route_url('mobile.home')
|
||||
else:
|
||||
referrer = request.route_url('home')
|
||||
return referrer
|
||||
|
|
|
@ -89,11 +89,7 @@ ${h.csrf_token(request)}
|
|||
<input type="reset" value="Reset" class="button" />
|
||||
% endif
|
||||
% if getattr(form, 'show_cancel', True):
|
||||
% if form.mobile:
|
||||
${h.link_to("Cancel", form.cancel_url, class_='ui-btn ui-corner-all')}
|
||||
% else:
|
||||
${h.link_to("Cancel", form.cancel_url, class_='cancel button{}'.format(' autodisable' if form.auto_disable_cancel else ''))}
|
||||
% endif
|
||||
${h.link_to("Cancel", form.cancel_url, class_='cancel button{}'.format(' autodisable' if form.auto_disable_cancel else ''))}
|
||||
% endif
|
||||
</div>
|
||||
% endif
|
||||
|
|
|
@ -65,18 +65,14 @@
|
|||
<input type="reset" value="Reset" class="button" />
|
||||
% endif
|
||||
% if getattr(form, 'show_cancel', True):
|
||||
% if form.mobile:
|
||||
${h.link_to("Cancel", form.cancel_url, class_='ui-btn ui-corner-all')}
|
||||
% if form.auto_disable_cancel:
|
||||
<once-button tag="a" href="${form.cancel_url or request.get_referrer()}"
|
||||
text="Cancel">
|
||||
</once-button>
|
||||
% else:
|
||||
% if form.auto_disable_cancel:
|
||||
<once-button tag="a" href="${form.cancel_url or request.get_referrer()}"
|
||||
text="Cancel">
|
||||
</once-button>
|
||||
% else:
|
||||
<b-button tag="a" href="${form.cancel_url or request.get_referrer()}">
|
||||
Cancel
|
||||
</b-button>
|
||||
% endif
|
||||
<b-button tag="a" href="${form.cancel_url or request.get_referrer()}">
|
||||
Cancel
|
||||
</b-button>
|
||||
% endif
|
||||
% endif
|
||||
</div>
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
## -*- coding: utf-8 -*-
|
||||
<%inherit file="/mobile/base.mako" />
|
||||
<%namespace name="base_meta" file="/base_meta.mako" />
|
||||
|
||||
<%def name="title()">About ${base_meta.app_title()}</%def>
|
||||
|
||||
<h2>${project_title} ${project_version}</h2>
|
||||
|
||||
% for name, version in packages.items():
|
||||
<h3>${name} ${version}</h3>
|
||||
% endfor
|
||||
|
||||
<p>Please see <a href="https://rattailproject.org/">rattailproject.org</a> for more info.</p>
|
|
@ -1,208 +0,0 @@
|
|||
## -*- coding: utf-8 -*-
|
||||
<%namespace name="base_meta" file="/base_meta.mako" />
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
|
||||
<title>${base_meta.global_title()} » ${self.title()}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
${self.jquery()}
|
||||
${h.javascript_link(request.static_url('tailbone:static/js/jquery.ui.tailbone.mobile.js') + '?ver={}'.format(tailbone.__version__))}
|
||||
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.mobile.js') + '?ver={}'.format(tailbone.__version__))}
|
||||
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.mobile.receiving.js') + '?ver={}'.format(tailbone.__version__))}
|
||||
${self.extra_javascript()}
|
||||
|
||||
## since jquery mobile will "utterly cache" the first page which is loaded
|
||||
## by the client, we must make sure that is always the home page. so if
|
||||
## user tries to e.g. "refresh" some other page, redirect to home page
|
||||
% if request.matched_route.name != 'mobile.home' and request.rattail_config.getbool('tailbone', 'mobile.force_home', default=True):
|
||||
<script type="text/javascript">
|
||||
location.href = '${request.route_url('mobile.home')}';
|
||||
</script>
|
||||
% endif
|
||||
|
||||
% if request.rattail_config.getbool('tailbone', 'mobile.flash.autodismiss', default=True):
|
||||
<script type="text/javascript">
|
||||
$(document).on('pageshow', function() {
|
||||
## TODO: seems like this should be better somehow...
|
||||
// remove all flash messages after 2.5 seconds
|
||||
window.setTimeout(function() { $('.flash, .error').remove(); }, 2500);
|
||||
});
|
||||
</script>
|
||||
% endif
|
||||
|
||||
${self.jquery_theme()}
|
||||
${h.stylesheet_link(request.static_url('tailbone:static/css/mobile.css') + '?ver={}'.format(tailbone.__version__))}
|
||||
% if not request.rattail_config.production():
|
||||
<style type="text/css">
|
||||
.ui-page-theme-a { background-image: url(${request.static_url('tailbone:static/img/testing.png')}); }
|
||||
</style>
|
||||
% endif
|
||||
${self.extra_styles()}
|
||||
|
||||
</head>
|
||||
${self.mobile_body()}
|
||||
</html>
|
||||
|
||||
<%def name="mobile_body()">
|
||||
<body>
|
||||
|
||||
## note that our toolbars are *external* (in jqm-speak) by default
|
||||
|
||||
${self.mobile_header()}
|
||||
|
||||
<div data-role="page" data-url="${self.page_url()}">
|
||||
|
||||
${self.mobile_usermenu()}
|
||||
|
||||
${self.mobile_page_body()}
|
||||
|
||||
</div><!-- page -->
|
||||
|
||||
${self.mobile_footer()}
|
||||
|
||||
</body>
|
||||
</%def>
|
||||
|
||||
<%def name="page_url()">${request.current_route_url()}</%def>
|
||||
|
||||
<%def name="page_title()">${self.title()}</%def>
|
||||
|
||||
<%def name="jquery()">
|
||||
${h.javascript_link('https://code.jquery.com/jquery-1.12.4.min.js')}
|
||||
${h.javascript_link('https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.js')}
|
||||
</%def>
|
||||
|
||||
<%def name="extra_javascript()"></%def>
|
||||
|
||||
<%def name="jquery_theme()">
|
||||
${h.stylesheet_link('https://code.jquery.com/mobile/1.4.5/jquery.mobile-1.4.5.min.css')}
|
||||
</%def>
|
||||
|
||||
<%def name="extra_styles()"></%def>
|
||||
|
||||
<%def name="mobile_header()">
|
||||
<div data-role="header">
|
||||
${self.mobile_header_link()}
|
||||
<h1>${base_meta.global_title()}</h1>
|
||||
${self.mobile_header_feedback()}
|
||||
</div>
|
||||
</%def>
|
||||
|
||||
<%def name="mobile_header_link()">
|
||||
<% classes = 'ui-btn-left ui-btn ui-btn-inline ui-mini ui-corner-all ui-btn-icon-left ' %>
|
||||
% if request.user:
|
||||
${h.link_to(request.user.get_short_name(), '#usermenu', data_role='button', data_icon='user',
|
||||
class_=' root-user' if request.is_root else '')}
|
||||
% elif request.matched_route.name in ('mobile.login', 'mobile.about'):
|
||||
${h.link_to("Home", url('mobile.home'), data_role='button', data_icon='home')}
|
||||
% else:
|
||||
${h.link_to("Login", url('mobile.login'), data_role='button', data_icon='user')}
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="mobile_header_feedback()">
|
||||
${h.link_to("Feedback", '#', id='feedback-button', data_role='button', data_icon='recycle')}
|
||||
</%def>
|
||||
|
||||
<%def name="mobile_usermenu()">
|
||||
<div id="usermenu" data-role="panel" data-display="overlay">
|
||||
<ul data-role="listview">
|
||||
<li data-icon="home">${h.link_to("Home", url('mobile.home'))}</li>
|
||||
% if request.has_perm('datasync.restart'):
|
||||
<li>${h.link_to("DataSync", url('datasync.mobile'))}</li>
|
||||
% endif
|
||||
% if request.is_root:
|
||||
<li class="root-user" data-icon="forbidden">${h.link_to("Stop being root", url('stop_root'), **{'data-ajax': 'false'})}</li>
|
||||
% elif request.is_admin:
|
||||
<li class="root-user" data-icon="forbidden">${h.link_to("Become root", url('become_root'), **{'data-ajax': 'false'})}</li>
|
||||
% endif
|
||||
<li data-icon="lock">${h.link_to("Logout", url('mobile.logout'), **{'data-ajax': 'false'})}</li>
|
||||
<li data-icon="info">${h.link_to("About {}".format(capture(base_meta.app_title)), url('mobile.about'))}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</%def>
|
||||
|
||||
<%def name="mobile_page_body()">
|
||||
<div role="main" class="ui-content" data-route="${request.matched_route.name}">
|
||||
|
||||
% if request.session.peek_flash('error'):
|
||||
% for error in request.session.pop_flash('error'):
|
||||
<div class="error">${error}</div>
|
||||
% endfor
|
||||
% endif
|
||||
|
||||
% if request.session.peek_flash():
|
||||
% for msg in request.session.pop_flash():
|
||||
<div class="flash">${msg|n}</div>
|
||||
% endfor
|
||||
% endif
|
||||
|
||||
<h2>${self.page_title()}</h2>
|
||||
|
||||
${self.body()}
|
||||
|
||||
<div data-role="popup" data-overlay-theme="b" id="feedback-popup" class="ui-content">
|
||||
<a href="#" data-rel="back" data-role="button" data-theme="a" data-icon="delete" data-iconpos="notext" class="ui-btn-right">Close</a>
|
||||
${self.mobile_feedback_form()}
|
||||
</div>
|
||||
|
||||
<div data-role="popup" data-overlay-theme="b" id="feedback-thanks" class="ui-content">
|
||||
Thank you for your feedback.
|
||||
</div>
|
||||
|
||||
<div class="replacement-header">
|
||||
${self.mobile_header_link()}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</%def>
|
||||
|
||||
<%def name="mobile_footer()">
|
||||
<div data-role="footer">
|
||||
<h4>powered by ${h.link_to("Rattail", url('mobile.about'))}</h4>
|
||||
</div>
|
||||
</%def>
|
||||
|
||||
<%def name="mobile_feedback_form()">
|
||||
${h.form(url('mobile.feedback'))}
|
||||
${h.csrf_token(request)}
|
||||
${h.hidden('user', value=request.user.uuid if request.user else None)}
|
||||
|
||||
<p>
|
||||
Questions, suggestions, comments, complaints, etc. <span class="red">regarding this website</span>
|
||||
are welcome and may be submitted below.
|
||||
</p>
|
||||
|
||||
<div class="field-wrapper referrer">
|
||||
<label for="referrer">Referring URL</label>
|
||||
<div class="field"></div>
|
||||
${h.hidden('referrer')}
|
||||
</div>
|
||||
|
||||
% if request.user:
|
||||
${h.hidden('user_name', value=six.text_type(request.user))}
|
||||
% else:
|
||||
<div class="field-wrapper user_name">
|
||||
<label for="user_name">Your Name</label>
|
||||
<div class="field">
|
||||
${h.text('user_name')}
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
<div class="field-wrapper message">
|
||||
<label for="message">Message</label>
|
||||
<div class="field">
|
||||
${h.textarea('message', cols=45, rows=15)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="buttons" id="feedback-form-buttons">
|
||||
<button type="button" data-inline="true" class="submit" data-theme="b">Send Note</button>
|
||||
<button type="button" data-inline="true" class="cancel">Cancel</button>
|
||||
</div>
|
||||
|
||||
${h.end_form()}
|
||||
</%def>
|
|
@ -1,20 +0,0 @@
|
|||
## -*- coding: utf-8 -*-
|
||||
<%inherit file="tailbone:templates/mobile/base.mako" />
|
||||
|
||||
<%def name="mobile_body()">
|
||||
<body>
|
||||
|
||||
<div data-role="page" data-url="${self.page_url()}"${' data-rel="dialog"' if dialog else ''|n}>
|
||||
|
||||
${self.mobile_usermenu()}
|
||||
|
||||
${self.mobile_header()}
|
||||
|
||||
${self.mobile_page_body()}
|
||||
|
||||
${self.mobile_footer()}
|
||||
|
||||
</div><!-- page -->
|
||||
|
||||
</body>
|
||||
</%def>
|
|
@ -1,10 +0,0 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/mobile/base.mako" />
|
||||
|
||||
<%def name="title()">${index_title} » ${instance_title} » Execute</%def>
|
||||
|
||||
<%def name="page_title()">${h.link_to(index_title, index_url)} » ${h.link_to(instance_title, instance_url)} » Execute</%def>
|
||||
|
||||
<div class="form-wrapper">
|
||||
${form.render()|n}
|
||||
</div><!-- form-wrapper -->
|
|
@ -1,6 +0,0 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/mobile/master/create.mako" />
|
||||
|
||||
<%def name="title()">${h.link_to("Inventory", url('mobile.batch.inventory'))} » New Batch</%def>
|
||||
|
||||
${parent.body()}
|
|
@ -1,6 +0,0 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/mobile/master/index.mako" />
|
||||
|
||||
<%def name="title()">Inventory</%def>
|
||||
|
||||
${parent.body()}
|
|
@ -1,24 +0,0 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/mobile/batch/view.mako" />
|
||||
|
||||
<%def name="title()">${h.link_to("Inventory", url('mobile.batch.inventory'))} » ${batch.id_str}</%def>
|
||||
|
||||
${form.render()|n}
|
||||
|
||||
% if not batch.executed and not batch.complete:
|
||||
<br />
|
||||
${h.text('upc-search', class_='inventory-upc-search', placeholder="Enter UPC", autocomplete='off', **{'data-type': 'search', 'data-url': url('mobile.batch.inventory.row_from_upc', uuid=batch.uuid)})}
|
||||
% endif
|
||||
|
||||
% if master.has_rows:
|
||||
<br />
|
||||
${grid.render_complete()|n}
|
||||
% endif
|
||||
|
||||
% if not batch.executed and not batch.complete:
|
||||
<br />
|
||||
${h.form(request.route_url('mobile.batch.inventory.mark_complete', uuid=batch.uuid))}
|
||||
${h.csrf_token(request)}
|
||||
${h.hidden('mark-complete', value='true')}
|
||||
<button type="submit">Mark Batch as Complete</button>
|
||||
% endif
|
|
@ -1,63 +0,0 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/mobile/batch/view_row.mako" />
|
||||
<%namespace file="/mobile/keypad.mako" import="keypad" />
|
||||
|
||||
## TODO: this is broken for actual page (header) title
|
||||
<%def name="title()">${h.link_to("Inventory", url('mobile.batch.inventory'))} » ${h.link_to(batch.id_str, url('mobile.batch.inventory.view', uuid=batch.uuid))} » ${row.upc.pretty()}</%def>
|
||||
|
||||
<div class="ui-grid-a">
|
||||
<div class="ui-block-a">
|
||||
% if instance.product:
|
||||
<h3>${row.brand_name or ""}</h3>
|
||||
<h3>${row.description} ${row.size}</h3>
|
||||
<h3>${h.pretty_quantity(row.case_quantity)} ${unit_uom} per CS</h3>
|
||||
% else:
|
||||
<h3>${row.description}</h3>
|
||||
% endif
|
||||
</div>
|
||||
<div class="ui-block-b">
|
||||
${h.image(product_image_url, "product image")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
currently:
|
||||
% if uom == 'CS':
|
||||
${h.pretty_quantity(row.cases or 0)}
|
||||
% else:
|
||||
${h.pretty_quantity(row.units or 0)}
|
||||
% endif
|
||||
${uom}
|
||||
</p>
|
||||
|
||||
% if not batch.executed and not batch.complete:
|
||||
|
||||
${h.form(request.current_route_url())}
|
||||
${h.csrf_token(request)}
|
||||
${h.hidden('row', value=row.uuid)}
|
||||
% if allow_cases:
|
||||
${h.hidden('cases')}
|
||||
% endif
|
||||
${h.hidden('units')}
|
||||
|
||||
<%
|
||||
quantity = 1
|
||||
if allow_cases:
|
||||
if row.cases is not None:
|
||||
quantity = row.cases
|
||||
elif row.units is not None:
|
||||
quantity = row.units
|
||||
elif row.units is not None:
|
||||
quantity = row.units
|
||||
%>
|
||||
${keypad(unit_uom, uom, quantity=quantity, allow_cases=allow_cases)}
|
||||
|
||||
<fieldset data-role="controlgroup" data-type="horizontal" class="inventory-actions">
|
||||
<button type="button" class="ui-btn-inline ui-corner-all save">Save</button>
|
||||
<button type="button" class="ui-btn-inline ui-corner-all delete" disabled="disabled">Delete</button>
|
||||
${h.link_to("Cancel", url('mobile.batch.inventory.view', uuid=batch.uuid), class_='ui-btn ui-btn-inline ui-corner-all')}
|
||||
</fieldset>
|
||||
|
||||
${h.end_form()}
|
||||
|
||||
% endif
|
|
@ -1,32 +0,0 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/mobile/master/view.mako" />
|
||||
|
||||
${parent.body()}
|
||||
|
||||
% if not batch.executed:
|
||||
% if request.has_perm('{}.edit'.format(permission_prefix)):
|
||||
% if batch.complete:
|
||||
${h.form(url('mobile.{}.mark_pending'.format(route_prefix), uuid=batch.uuid))}
|
||||
${h.csrf_token(request)}
|
||||
${h.hidden('mark-pending', value='true')}
|
||||
${h.submit('submit', "Mark Batch as Pending")}
|
||||
${h.end_form()}
|
||||
% else:
|
||||
${h.form(url('mobile.{}.mark_complete'.format(route_prefix), uuid=batch.uuid))}
|
||||
${h.csrf_token(request)}
|
||||
${h.hidden('mark-complete', value='true')}
|
||||
${h.submit('submit', "Mark Batch as Complete")}
|
||||
${h.end_form()}
|
||||
% endif
|
||||
% endif
|
||||
% if batch.complete and master.mobile_executable and request.has_perm('{}.execute'.format(permission_prefix)):
|
||||
% if master.has_execution_options(batch):
|
||||
${h.link_to("Execute Batch", url('mobile.{}.execute'.format(route_prefix), uuid=batch.uuid), class_='ui-btn ui-corner-all')}
|
||||
% else:
|
||||
${h.form(url('mobile.{}.execute'.format(route_prefix), uuid=batch.uuid))}
|
||||
${h.csrf_token(request)}
|
||||
${h.submit('submit', "Execute Batch")}
|
||||
${h.end_form()}
|
||||
% endif
|
||||
% endif
|
||||
% endif
|
|
@ -1,4 +0,0 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/mobile/master/view_row.mako" />
|
||||
|
||||
${parent.body()}
|
|
@ -1,9 +0,0 @@
|
|||
## -*- coding: utf-8 -*-
|
||||
<%inherit file="/mobile/base.mako" />
|
||||
|
||||
<%def name="title()">DataSync</%def>
|
||||
|
||||
${h.form(url('datasync.restart'))}
|
||||
${h.csrf_token(request)}
|
||||
${h.submit('restart', "Restart DataSync Daemon", id='datasync-restart')}
|
||||
${h.end_form()}
|
|
@ -1,7 +0,0 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
|
||||
% if grid.filterable:
|
||||
${grid.render_filters()|n}
|
||||
% endif
|
||||
|
||||
${grid.render_grid()|n}
|
|
@ -1,15 +0,0 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<div class="simple-filter">
|
||||
${h.form(request.current_route_url(_query=None), method='get')}
|
||||
|
||||
% for filtr in grid.iter_filters():
|
||||
${h.hidden('{}.verb'.format(filtr.key), value=filtr.verb)}
|
||||
<fieldset data-role="controlgroup" data-type="horizontal">
|
||||
% for value, label in filtr.iter_choices():
|
||||
${h.radio(filtr.key, value=value, label=label, checked=value == filtr.value)}
|
||||
% endfor
|
||||
</fieldset>
|
||||
% endfor
|
||||
|
||||
${h.end_form()}
|
||||
</div><!-- simple-filter -->
|
|
@ -1,36 +0,0 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
|
||||
<ul data-role="listview">
|
||||
${grid.make_webhelpers_grid()}
|
||||
</ul>
|
||||
|
||||
## <table data-role="table" class="ui-responsive table-stroke">
|
||||
## <thead>
|
||||
## <tr>
|
||||
## % for column in grid.iter_visible_columns():
|
||||
## ${grid.column_header(column)}
|
||||
## % endfor
|
||||
## </tr>
|
||||
## </thead>
|
||||
## <tbody>
|
||||
## % for i, row in enumerate(grid.iter_rows(), 1):
|
||||
## <tr>
|
||||
## % for column in grid.iter_visible_columns():
|
||||
## <td>${grid.render_cell(row, column)}</td>
|
||||
## % endfor
|
||||
## </tr>
|
||||
## % endfor
|
||||
## </tbody>
|
||||
## </table>
|
||||
|
||||
% if grid.pageable and grid.pager:
|
||||
<br />
|
||||
<div data-role="controlgroup" data-type="horizontal">
|
||||
${grid.pager.pager('$link_first $link_previous $link_next $link_last',
|
||||
symbol_first='<< first', symbol_last='last >>',
|
||||
symbol_previous='< prev', symbol_next='next >',
|
||||
link_attr={'class': 'ui-btn ui-corner-all'},
|
||||
curpage_attr={'class': 'ui-btn ui-corner-all'},
|
||||
dotdot_attr={'class': 'ui-btn ui-corner-all'})|n}
|
||||
</div>
|
||||
% endif
|
|
@ -1,12 +0,0 @@
|
|||
## -*- coding: utf-8 -*-
|
||||
<%inherit file="/mobile/base.mako" />
|
||||
<%namespace name="base_meta" file="/base_meta.mako" />
|
||||
|
||||
<%def name="title()">Home</%def>
|
||||
|
||||
<%def name="page_title()"></%def>
|
||||
|
||||
<div style="text-align: center;">
|
||||
${h.image(image_url, "{} logo".format(capture(base_meta.app_title)), id='logo', width=300)}
|
||||
<h3>Welcome to ${base_meta.app_title()}</h3>
|
||||
</div>
|
|
@ -1,41 +0,0 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
|
||||
<%def name="keypad(unit_uom, selected_uom, quantity=1, allow_cases=True)">
|
||||
<div class="quantity-keypad-thingy" data-changed="false">
|
||||
|
||||
<table>
|
||||
<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>
|
||||
|
||||
<fieldset data-role="controlgroup" data-type="horizontal">
|
||||
<button type="button" class="ui-btn-active keypad-quantity">${h.pretty_quantity(1 if quantity is None else quantity)}</button>
|
||||
<button type="button" disabled="disabled"> </button>
|
||||
% if allow_cases:
|
||||
${h.radio('keypad-uom', value='CS', checked=selected_uom == 'CS', label="CS")}
|
||||
% endif
|
||||
${h.radio('keypad-uom', value=unit_uom, checked=selected_uom == unit_uom, label=unit_uom)}
|
||||
</fieldset>
|
||||
|
||||
</div>
|
||||
</%def>
|
|
@ -1,7 +0,0 @@
|
|||
## -*- coding: utf-8 -*-
|
||||
<%inherit file="/mobile/base.mako" />
|
||||
<%namespace file="/login.mako" import="login_form" />
|
||||
|
||||
<%def name="title()">Login</%def>
|
||||
|
||||
${login_form()}
|
|
@ -1,8 +0,0 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/mobile/base.mako" />
|
||||
|
||||
<%def name="title()">New ${model_title}</%def>
|
||||
|
||||
<div class="form-wrapper">
|
||||
${form.render()|n}
|
||||
</div><!-- form-wrapper -->
|
|
@ -1,6 +0,0 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/mobile/master/create.mako" />
|
||||
|
||||
<%def name="title()">New ${model_title} Row</%def>
|
||||
|
||||
${parent.body()}
|
|
@ -1,10 +0,0 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/mobile/base.mako" />
|
||||
|
||||
<%def name="title()">${index_title} » ${instance_title} » Edit</%def>
|
||||
|
||||
<%def name="page_title()">${h.link_to(index_title, index_url)} » ${h.link_to(instance_title, instance_url)} » Edit</%def>
|
||||
|
||||
<div class="form-wrapper">
|
||||
${form.render()|n}
|
||||
</div><!-- form-wrapper -->
|
|
@ -1,17 +0,0 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/mobile/master/edit.mako" />
|
||||
|
||||
<%def name="title()">${index_title} » ${parent_title} » ${instance_title} » Edit</%def>
|
||||
|
||||
<%def name="page_title()">${h.link_to(index_title, index_url)} » ${h.link_to(parent_title, parent_url)} » ${h.link_to(instance_title, instance_url)} » Edit</%def>
|
||||
|
||||
<div class="form-wrapper">
|
||||
${form.render()|n}
|
||||
</div><!-- form-wrapper -->
|
||||
|
||||
% if master.mobile_rows_deletable and request.has_perm('{}.delete_row'.format(permission_prefix)):
|
||||
${h.form(url('mobile.{}.delete_row'.format(route_prefix), uuid=parent_instance.uuid, row_uuid=row.uuid))}
|
||||
${h.csrf_token(request)}
|
||||
${h.submit('submit', "Delete this Row")}
|
||||
${h.end_form()}
|
||||
% endif
|
|
@ -1,17 +0,0 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
## ##############################################################################
|
||||
##
|
||||
## Default master 'index' template for mobile. Features a somewhat abbreviated
|
||||
## data table and (hopefully) exposes a way to filter and sort the data, etc.
|
||||
##
|
||||
## ##############################################################################
|
||||
<%inherit file="/mobile/base.mako" />
|
||||
|
||||
<%def name="title()">${index_title}</%def>
|
||||
|
||||
% if master.mobile_creatable and request.has_perm('{}.create'.format(permission_prefix)):
|
||||
${h.link_to("New {}".format(model_title), url('mobile.{}.create'.format(route_prefix)), class_='ui-btn ui-corner-all')}
|
||||
<br />
|
||||
% endif
|
||||
|
||||
${grid.render_complete()|n}
|
|
@ -1,48 +0,0 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
## ##############################################################################
|
||||
##
|
||||
## Default master 'view' template for mobile. Features a basic field list, and
|
||||
## links to edit/delete the object when appropriate.
|
||||
##
|
||||
## ##############################################################################
|
||||
<%inherit file="/mobile/base.mako" />
|
||||
|
||||
<%def name="title()">${index_title} » ${instance_title}</%def>
|
||||
|
||||
<%def name="page_title()">${h.link_to(index_title, index_url)} » ${instance_title}</%def>
|
||||
|
||||
${form.render()|n}
|
||||
|
||||
% if master.has_rows:
|
||||
|
||||
% if master.mobile_rows_creatable and master.rows_creatable_for(instance):
|
||||
## TODO: this seems like a poor choice of names? what are we really testing for here?
|
||||
% if master.mobile_rows_creatable_via_browse:
|
||||
<% add_title = "Add Record" if add_item_title is Undefined else add_item_title %>
|
||||
${h.link_to(add_title, url('mobile.{}.create_row'.format(route_prefix), uuid=instance.uuid), class_='ui-btn ui-corner-all')}
|
||||
% endif
|
||||
% endif
|
||||
% if master.mobile_rows_quickable and master.rows_quickable_for(instance):
|
||||
<% placeholder = '' if quick_entry_placeholder is Undefined else quick_entry_placeholder %>
|
||||
${h.form(url('mobile.{}.quick_row'.format(route_prefix), uuid=instance.uuid))}
|
||||
${h.csrf_token(request)}
|
||||
% if quick_row_autocomplete:
|
||||
<div class="field autocomplete quick-row" data-url="${quick_row_autocomplete_url}">
|
||||
${h.hidden('quick_entry')}
|
||||
${h.text('quick_row_autocomplete_text', placeholder=placeholder, autocomplete='off', data_type='search')}
|
||||
<ul data-role="listview" data-inset="true" data-filter="true" data-input="#quick_row_autocomplete_text"></ul>
|
||||
<button type="button" style="display: none;">Change</button>
|
||||
</div>
|
||||
% else:
|
||||
${h.text('quick_entry', placeholder=placeholder, autocomplete='off', **{'data-type': 'search', 'data-url': url('mobile.{}.quick_row'.format(route_prefix), uuid=instance.uuid), 'data-wedge': 'true' if quick_row_keyboard_wedge else 'false'})}
|
||||
% endif
|
||||
${h.end_form()}
|
||||
% endif
|
||||
|
||||
<br />
|
||||
${grid.render_complete()|n}
|
||||
% endif
|
||||
|
||||
% if master.mobile_editable and instance_editable and request.has_perm('{}.edit'.format(permission_prefix)):
|
||||
${h.link_to("Edit This", url('mobile.{}.edit'.format(route_prefix), uuid=instance.uuid), class_='ui-btn ui-corner-all')}
|
||||
% endif
|
|
@ -1,19 +0,0 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/mobile/master/view.mako" />
|
||||
|
||||
<%def name="title()">${index_title} » ${parent_title} » ${instance_title}</%def>
|
||||
|
||||
<%def name="page_title()">${h.link_to(index_title, index_url)} » ${h.link_to(parent_title, parent_url)} » ${instance_title}</%def>
|
||||
|
||||
${form.render()|n}
|
||||
|
||||
% if master.mobile_rows_editable and instance_editable and request.has_perm('{}.edit_row'.format(permission_prefix)):
|
||||
${h.link_to("Edit", url('mobile.{}.edit_row'.format(route_prefix), uuid=instance.batch_uuid, row_uuid=instance.uuid), class_='ui-btn')}
|
||||
% endif
|
||||
|
||||
% 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=parent_instance.uuid, row_uuid=row.uuid))}
|
||||
${h.csrf_token(request)}
|
||||
${h.submit('submit', "Delete this Row")}
|
||||
${h.end_form()}
|
||||
% endif
|
|
@ -1,31 +0,0 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/mobile/base.mako" />
|
||||
|
||||
<%def name="title()">${index_title} » New Batch</%def>
|
||||
|
||||
<%def name="page_title()">${h.link_to(index_title, index_url)} » New Batch</%def>
|
||||
|
||||
${h.form(request.current_route_url(), class_='ui-filterable', name='new-purchasing-batch')}
|
||||
${h.csrf_token(request)}
|
||||
|
||||
<div class="field-wrapper vendor">
|
||||
% if vendor_use_autocomplete:
|
||||
<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>
|
||||
% else:
|
||||
<div class="field-row">
|
||||
<label for="vendor">Vendor</label>
|
||||
<div class="field">
|
||||
${h.select('vendor', None, vendor_options)}
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
</div>
|
||||
|
||||
<br />
|
||||
${h.submit('submit', "Make Batch")}
|
||||
${h.end_form()}
|
|
@ -1,6 +0,0 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/mobile/master/create_row.mako" />
|
||||
|
||||
<%def name="page_title()">${h.link_to(index_title, index_url)} » ${h.link_to(instance_title, instance_url)} » Add Item</%def>
|
||||
|
||||
${parent.body()}
|
|
@ -1,6 +0,0 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/mobile/master/create_row.mako" />
|
||||
|
||||
<%def name="page_title()">${h.link_to(index_title, index_url)} » ${h.link_to(instance_title, instance_url)} » Add Item</%def>
|
||||
|
||||
${parent.body()}
|
|
@ -1,17 +0,0 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/mobile/master/index.mako" />
|
||||
|
||||
% if master.mobile_creatable and request.has_perm('{}.create'.format(permission_prefix)):
|
||||
${h.link_to("New {}".format(model_title), url('mobile.{}.create'.format(route_prefix)), class_='ui-btn ui-corner-all')}
|
||||
% endif
|
||||
|
||||
% if quick_lookup:
|
||||
|
||||
${h.form(url('mobile.{}.quick_lookup'.format(route_prefix)))}
|
||||
${h.csrf_token(request)}
|
||||
${h.text('quick_entry', placeholder=placeholder, autocomplete='off', **{'data-type': 'search', 'data-url': url('mobile.{}.quick_lookup'.format(route_prefix)), 'data-wedge': 'true' if quick_lookup_keyboard_wedge else 'false'})}
|
||||
${h.end_form()}
|
||||
|
||||
% else: ## not quick_only
|
||||
${grid.render_complete()|n}
|
||||
% endif
|
|
@ -1,85 +0,0 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/mobile/base.mako" />
|
||||
|
||||
<%def name="title()">Receiving » New Batch</%def>
|
||||
|
||||
<%def name="page_title()">${h.link_to("Receiving", url('mobile.receiving'))} » New Batch</%def>
|
||||
|
||||
${h.form(form.action_url, class_='ui-filterable', name='new-receiving-batch')}
|
||||
${h.csrf_token(request)}
|
||||
|
||||
% if phase == 1:
|
||||
|
||||
% if vendor_use_autocomplete:
|
||||
<div class="field-wrapper vendor">
|
||||
<div class="field autocomplete" data-url="${url('vendors.autocomplete')}">
|
||||
${h.hidden('vendor')}
|
||||
${h.text('new-receiving-batch-vendor-text', placeholder="Vendor name", autocomplete='off', **{'data-type': 'search'})}
|
||||
<ul data-role="listview" data-inset="true" data-filter="true" data-input="#new-receiving-batch-vendor-text"></ul>
|
||||
<button type="button" style="display: none;">Change Vendor</button>
|
||||
</div>
|
||||
</div>
|
||||
% else:
|
||||
<div class="field-row">
|
||||
<label for="vendor">Vendor</label>
|
||||
<div class="field">
|
||||
${h.select('vendor', None, vendor_options)}
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
|
||||
<br />
|
||||
|
||||
<div id="new-receiving-types" style="display: none;">
|
||||
|
||||
${h.hidden('workflow')}
|
||||
${h.hidden('phase', value='1')}
|
||||
|
||||
% if master.allow_from_po:
|
||||
<button type="button" class="start-receiving" data-workflow="from_po">Receive from PO</button>
|
||||
% endif
|
||||
|
||||
% if master.allow_from_scratch:
|
||||
<button type="button" class="start-receiving" data-workflow="from_scratch">Receive from Scratch</button>
|
||||
% endif
|
||||
|
||||
% if master.allow_truck_dump:
|
||||
<button type="button" class="start-receiving" data-workflow="truck_dump">Receive Truck Dump</button>
|
||||
% endif
|
||||
|
||||
</div>
|
||||
|
||||
% else: ## phase 2
|
||||
|
||||
${h.hidden('workflow')}
|
||||
${h.hidden('phase', value='2')}
|
||||
|
||||
<div class="field-wrapper vendor">
|
||||
<label>Vendor</label>
|
||||
<div class="field">
|
||||
${h.hidden('vendor', value=vendor.uuid)}
|
||||
${vendor}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
% if purchases:
|
||||
${h.hidden(purchase_order_fieldname, class_='purchase-order-field')}
|
||||
<p>Please choose a Purchase Order to receive:</p>
|
||||
<ul data-role="listview" data-inset="true">
|
||||
% for key, purchase in purchases:
|
||||
<li data-key="${key}">${h.link_to(purchase, '#')}</li>
|
||||
% endfor
|
||||
</ul>
|
||||
% else:
|
||||
<p>(no eligible purchases found)</p>
|
||||
% endif
|
||||
|
||||
% if master.allow_from_scratch:
|
||||
<button type="button" class="start-receiving" data-workflow="from_scratch">Receive from Scratch</button>
|
||||
% endif
|
||||
|
||||
${h.link_to("Cancel", url('mobile.{}'.format(route_prefix)), class_='ui-btn ui-corner-all')}
|
||||
|
||||
% endif
|
||||
|
||||
${h.end_form()}
|
|
@ -1,151 +0,0 @@
|
|||
## -*- 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>
|
||||
|
||||
<%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)}</%def>
|
||||
|
||||
|
||||
<div${' class="ui-grid-a"' if product_image_url else ''|n}>
|
||||
<div class="ui-block-a"${'' if instance.product else ' style="background-color: red;"'|n}>
|
||||
% if instance.product:
|
||||
<h3>${instance.brand_name or ""}</h3>
|
||||
<h3>${instance.description} ${instance.size or ''}</h3>
|
||||
% if allow_cases:
|
||||
<h3>1 CS = ${h.pretty_quantity(row.case_quantity or 1)} ${unit_uom}</h3>
|
||||
% endif
|
||||
% else:
|
||||
<h3>${instance.description}</h3>
|
||||
% endif
|
||||
</div>
|
||||
% if product_image_url:
|
||||
<div class="ui-block-b">
|
||||
${h.image(product_image_url, "product image")}
|
||||
</div>
|
||||
% endif
|
||||
</div>
|
||||
|
||||
<table${'' if instance.product else ' style="background-color: red;"'|n}>
|
||||
<tbody>
|
||||
% if batch.order_quantities_known:
|
||||
<tr>
|
||||
<td>shipped</td>
|
||||
<td>
|
||||
% if allow_cases:
|
||||
${h.pretty_quantity(row.cases_shipped or 0)} /
|
||||
% endif
|
||||
${h.pretty_quantity(row.units_shipped or 0)}
|
||||
</td>
|
||||
</tr>
|
||||
% endif
|
||||
<tr>
|
||||
<td>received</td>
|
||||
<td>
|
||||
% if allow_cases:
|
||||
${h.pretty_quantity(row.cases_received or 0)} /
|
||||
% endif
|
||||
${h.pretty_quantity(row.units_received or 0)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>damaged</td>
|
||||
<td>
|
||||
% if allow_cases:
|
||||
${h.pretty_quantity(row.cases_damaged or 0)} /
|
||||
% endif
|
||||
${h.pretty_quantity(row.units_damaged or 0)}
|
||||
</td>
|
||||
</tr>
|
||||
% if allow_expired:
|
||||
<tr>
|
||||
<td>expired</td>
|
||||
<td>
|
||||
% if allow_cases:
|
||||
${h.pretty_quantity(row.cases_expired or 0)} /
|
||||
% endif
|
||||
${h.pretty_quantity(row.units_expired or 0)}
|
||||
</td>
|
||||
</tr>
|
||||
% endif
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
% if request.session.peek_flash('receiving-warning'):
|
||||
% for error in request.session.pop_flash('receiving-warning'):
|
||||
<div class="receiving-warning">${error}</div>
|
||||
% 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:
|
||||
<button type="button" class="quick-receive" data-quantity="${quick_receive_quantity}" data-uom="${quick_receive_uom}">${quick_receive_text}</button>
|
||||
% elif allow_cases:
|
||||
<button type="button" class="quick-receive" data-quantity="1" data-uom="CS">Receive 1 CS</button>
|
||||
<div>
|
||||
## TODO: probably should make these optional / configurable
|
||||
<button type="button" class="quick-receive ui-btn ui-btn-inline ui-corner-all" data-quantity="1" data-uom="EA">1 EA</button>
|
||||
<button type="button" class="quick-receive ui-btn ui-btn-inline ui-corner-all" data-quantity="3" data-uom="EA">3 EA</button>
|
||||
<button type="button" class="quick-receive ui-btn ui-btn-inline ui-corner-all" data-quantity="6" data-uom="EA">6 EA</button>
|
||||
</div>
|
||||
<br />
|
||||
% else:
|
||||
<button type="button" class="quick-receive" data-quantity="1" data-uom="${unit_uom}">Receive 1 ${unit_uom}</button>
|
||||
% endif
|
||||
% endif
|
||||
|
||||
${keypad(unit_uom, uom, allow_cases=allow_cases)}
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<fieldset data-role="controlgroup" data-type="horizontal" class="receiving-mode">
|
||||
${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
|
||||
</fieldset>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="expiration-row" style="display: none;">
|
||||
<td>
|
||||
<div style="padding:10px 20px;">
|
||||
<label for="expiration_date">Expiration Date</label>
|
||||
<input name="expiration_date" type="date" value="" placeholder="YYYY-MM-DD" />
|
||||
</div>
|
||||
</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.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
|
|
@ -1,151 +0,0 @@
|
|||
## -*- 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>
|
||||
|
||||
<%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)}</%def>
|
||||
|
||||
|
||||
<div${' class="ui-grid-a"' if product_image_url else ''|n}>
|
||||
<div class="ui-block-a"${'' if instance.product else ' style="background-color: red;"'|n}>
|
||||
% if instance.product:
|
||||
<h3>${instance.brand_name or ""}</h3>
|
||||
<h3>${instance.description} ${instance.size or ''}</h3>
|
||||
% if allow_cases:
|
||||
<h3>1 CS = ${h.pretty_quantity(row.case_quantity or 1)} ${unit_uom}</h3>
|
||||
% endif
|
||||
% else:
|
||||
<h3>${instance.description}</h3>
|
||||
% endif
|
||||
</div>
|
||||
% if product_image_url:
|
||||
<div class="ui-block-b">
|
||||
${h.image(product_image_url, "product image")}
|
||||
</div>
|
||||
% endif
|
||||
</div>
|
||||
|
||||
<table${'' if instance.product else ' style="background-color: red;"'|n}>
|
||||
<tbody>
|
||||
% if batch.order_quantities_known:
|
||||
<tr>
|
||||
<td>ordered</td>
|
||||
<td>
|
||||
% if allow_cases:
|
||||
${h.pretty_quantity(row.cases_ordered or 0)} /
|
||||
% endif
|
||||
${h.pretty_quantity(row.units_ordered or 0)}
|
||||
</td>
|
||||
</tr>
|
||||
% endif
|
||||
<tr>
|
||||
<td>received</td>
|
||||
<td>
|
||||
% if allow_cases:
|
||||
${h.pretty_quantity(row.cases_received or 0)} /
|
||||
% endif
|
||||
${h.pretty_quantity(row.units_received or 0)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>damaged</td>
|
||||
<td>
|
||||
% if allow_cases:
|
||||
${h.pretty_quantity(row.cases_damaged or 0)} /
|
||||
% endif
|
||||
${h.pretty_quantity(row.units_damaged or 0)}
|
||||
</td>
|
||||
</tr>
|
||||
% if allow_expired:
|
||||
<tr>
|
||||
<td>expired</td>
|
||||
<td>
|
||||
% if allow_cases:
|
||||
${h.pretty_quantity(row.cases_expired or 0)} /
|
||||
% endif
|
||||
${h.pretty_quantity(row.units_expired or 0)}
|
||||
</td>
|
||||
</tr>
|
||||
% endif
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
% if request.session.peek_flash('receiving-warning'):
|
||||
% for error in request.session.pop_flash('receiving-warning'):
|
||||
<div class="receiving-warning">${error}</div>
|
||||
% 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:
|
||||
<button type="button" class="quick-receive" data-quantity="${quick_receive_quantity}" data-uom="${quick_receive_uom}">${quick_receive_text}</button>
|
||||
% elif allow_cases:
|
||||
<button type="button" class="quick-receive" data-quantity="1" data-uom="CS">Receive 1 CS</button>
|
||||
<div>
|
||||
## TODO: probably should make these optional / configurable
|
||||
<button type="button" class="quick-receive ui-btn ui-btn-inline ui-corner-all" data-quantity="1" data-uom="EA">1 EA</button>
|
||||
<button type="button" class="quick-receive ui-btn ui-btn-inline ui-corner-all" data-quantity="3" data-uom="EA">3 EA</button>
|
||||
<button type="button" class="quick-receive ui-btn ui-btn-inline ui-corner-all" data-quantity="6" data-uom="EA">6 EA</button>
|
||||
</div>
|
||||
<br />
|
||||
% else:
|
||||
<button type="button" class="quick-receive" data-quantity="1" data-uom="${unit_uom}">Receive 1 ${unit_uom}</button>
|
||||
% endif
|
||||
% endif
|
||||
|
||||
${keypad(unit_uom, uom, allow_cases=allow_cases)}
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<fieldset data-role="controlgroup" data-type="horizontal" class="receiving-mode">
|
||||
${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
|
||||
</fieldset>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="expiration-row" style="display: none;">
|
||||
<td>
|
||||
<div style="padding:10px 20px;">
|
||||
<label for="expiration_date">Expiration Date</label>
|
||||
<input name="expiration_date" type="date" value="" placeholder="YYYY-MM-DD" />
|
||||
</div>
|
||||
</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.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
|
|
@ -1,311 +0,0 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%namespace file="/grids/nav.mako" import="grid_index_nav" />
|
||||
<%namespace file="/feedback_dialog.mako" import="feedback_dialog" />
|
||||
<%namespace name="base_meta" file="/base_meta.mako" />
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
|
||||
<title>${base_meta.global_title()} » ${capture(self.title)|n}</title>
|
||||
${base_meta.favicon()}
|
||||
${self.header_core()}
|
||||
|
||||
% if background_color:
|
||||
<style type="text/css">
|
||||
body, .navbar, .footer {
|
||||
background-color: ${background_color};
|
||||
}
|
||||
</style>
|
||||
% endif
|
||||
|
||||
% if not request.rattail_config.production():
|
||||
<style type="text/css">
|
||||
body, .navbar, .footer {
|
||||
background-image: url(${request.static_url('tailbone:static/img/testing.png')});
|
||||
}
|
||||
</style>
|
||||
% endif
|
||||
|
||||
${self.head_tags()}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
|
||||
<nav class="navbar" role="navigation" aria-label="main navigation">
|
||||
<div class="navbar-menu">
|
||||
<div class="navbar-start">
|
||||
|
||||
% for topitem in menus:
|
||||
% if topitem.is_link:
|
||||
${h.link_to(topitem.title, topitem.url, target=topitem.target, class_='navbar-item')}
|
||||
% else:
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link">${topitem.title}</a>
|
||||
<div class="navbar-dropdown">
|
||||
% for subitem in topitem.items:
|
||||
% if subitem.is_sep:
|
||||
<hr class="navbar-divider">
|
||||
% else:
|
||||
${h.link_to(subitem.title, subitem.url, class_='navbar-item', target=subitem.target)}
|
||||
% endif
|
||||
% endfor
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
% endfor
|
||||
|
||||
</div><!-- navbar-start -->
|
||||
<div class="navbar-end">
|
||||
|
||||
## User Menu
|
||||
% if request.user:
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
% if messaging_enabled:
|
||||
<a class="navbar-link ${'root-user' if request.is_root else ''}">${request.user}${" ({})".format(inbox_count) if inbox_count else ''}</a>
|
||||
% else:
|
||||
<a class="navbar-link ${'root-user' if request.is_root else ''}">${request.user}</a>
|
||||
% endif
|
||||
<div class="navbar-dropdown">
|
||||
% if request.is_root:
|
||||
${h.link_to("Stop being root", url('stop_root'), class_='navbar-item root-user')}
|
||||
% elif request.is_admin:
|
||||
${h.link_to("Become root", url('become_root'), class_='navbar-item root-user')}
|
||||
% endif
|
||||
% if messaging_enabled:
|
||||
${h.link_to("Messages{}".format(" ({})".format(inbox_count) if inbox_count else ''), url('messages.inbox'), class_='navbar-item')}
|
||||
% endif
|
||||
${h.link_to("Change Password", url('change_password'), class_='navbar-item')}
|
||||
${h.link_to("Logout", url('logout'), class_='navbar-item')}
|
||||
</div>
|
||||
</div>
|
||||
% else:
|
||||
${h.link_to("Login", url('login'), class_='navbar-item')}
|
||||
% endif
|
||||
|
||||
</div><!-- navbar-end -->
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<nav class="level">
|
||||
<div class="level-left">
|
||||
|
||||
## App Logo / Name
|
||||
<div class="level-item">
|
||||
<a class="home" href="${url('home')}">
|
||||
<div id="header-logo">${base_meta.header_logo()}</div>
|
||||
<span class="global-title">${base_meta.global_title()}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
## Current Context
|
||||
<div id="current-context" class="level-item">
|
||||
% if master:
|
||||
<span>»</span>
|
||||
% if master.listing:
|
||||
<span>${index_title}</span>
|
||||
% else:
|
||||
${h.link_to(index_title, index_url)}
|
||||
% if parent_url is not Undefined:
|
||||
<span>»</span>
|
||||
${h.link_to(parent_title, parent_url)}
|
||||
% elif instance_url is not Undefined:
|
||||
<span>»</span>
|
||||
${h.link_to(instance_title, instance_url)}
|
||||
% endif
|
||||
% if master.viewing and grid_index:
|
||||
${grid_index_nav()}
|
||||
% endif
|
||||
% endif
|
||||
% elif index_title:
|
||||
<span>»</span>
|
||||
<span>${index_title}</span>
|
||||
% endif
|
||||
</div>
|
||||
|
||||
</div><!-- level-left -->
|
||||
<div class="level-right">
|
||||
|
||||
## Theme Picker
|
||||
% if expose_theme_picker and request.has_perm('common.change_app_theme'):
|
||||
<div class="level-item">
|
||||
${h.form(url('change_theme'), method="post")}
|
||||
${h.csrf_token(request)}
|
||||
Theme:
|
||||
<div class="theme-picker">
|
||||
<div class="select">
|
||||
${h.select('theme', theme, options=theme_picker_options, id='theme-picker')}
|
||||
</div>
|
||||
</div>
|
||||
${h.end_form()}
|
||||
</div>
|
||||
% endif
|
||||
|
||||
## Help Button
|
||||
% if help_url is not Undefined and help_url:
|
||||
<div class="level-item">
|
||||
${h.link_to("Help", help_url, target='_blank', class_='button')}
|
||||
</div>
|
||||
% endif
|
||||
|
||||
## Feedback Button
|
||||
<div class="level-item">
|
||||
<button type="button" class="button is-primary" id="feedback">Feedback</button>
|
||||
</div>
|
||||
|
||||
</div><!-- level-right -->
|
||||
</nav><!-- level -->
|
||||
</header>
|
||||
|
||||
## Page Title
|
||||
<section id="content-title" class="hero is-primary">
|
||||
<div class="container">
|
||||
% if capture(self.content_title):
|
||||
|
||||
% if show_prev_next is not Undefined and show_prev_next:
|
||||
<div style="float: right;">
|
||||
% if prev_url:
|
||||
${h.link_to("« Older", prev_url, class_='button autodisable')}
|
||||
% else:
|
||||
${h.link_to("« Older", '#', class_='button', disabled='disabled')}
|
||||
% endif
|
||||
% if next_url:
|
||||
${h.link_to("Newer »", next_url, class_='button autodisable')}
|
||||
% else:
|
||||
${h.link_to("Newer »", '#', class_='button', disabled='disabled')}
|
||||
% endif
|
||||
</div>
|
||||
% endif
|
||||
|
||||
<h1 class="title">${self.content_title()}</h1>
|
||||
% endif
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="content-wrapper">
|
||||
|
||||
## Page Body
|
||||
<section id="page-body">
|
||||
|
||||
% if request.session.peek_flash('error'):
|
||||
% for error in request.session.pop_flash('error'):
|
||||
<div class="notification is-warning">
|
||||
<!-- <button class="delete"></button> -->
|
||||
${error}
|
||||
</div>
|
||||
% endfor
|
||||
% endif
|
||||
|
||||
% if request.session.peek_flash():
|
||||
% for msg in request.session.pop_flash():
|
||||
<div class="notification is-info">
|
||||
<!-- <button class="delete"></button> -->
|
||||
${msg}
|
||||
</div>
|
||||
% endfor
|
||||
% endif
|
||||
|
||||
${self.body()}
|
||||
</section>
|
||||
|
||||
## Feedback Dialog
|
||||
${feedback_dialog()}
|
||||
|
||||
## Footer
|
||||
<footer class="footer">
|
||||
<div class="content">
|
||||
${base_meta.footer()}
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</div><!-- content-wrapper -->
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<%def name="title()"></%def>
|
||||
|
||||
<%def name="content_title()">
|
||||
${self.title()}
|
||||
</%def>
|
||||
|
||||
<%def name="header_core()">
|
||||
|
||||
${self.core_javascript()}
|
||||
${self.extra_javascript()}
|
||||
${self.core_styles()}
|
||||
${self.extra_styles()}
|
||||
|
||||
## TODO: should this be elsewhere / more customizable?
|
||||
% if dform is not Undefined:
|
||||
<% resources = dform.get_widget_resources() %>
|
||||
% for path in resources['js']:
|
||||
${h.javascript_link(request.static_url(path))}
|
||||
% endfor
|
||||
% for path in resources['css']:
|
||||
${h.stylesheet_link(request.static_url(path))}
|
||||
% endfor
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="core_javascript()">
|
||||
${self.jquery()}
|
||||
${h.javascript_link(request.static_url('tailbone:static/js/lib/jquery.loadmask.min.js'))}
|
||||
${h.javascript_link(request.static_url('tailbone:static/js/lib/jquery.ui.timepicker.js'))}
|
||||
<script type="text/javascript">
|
||||
var session_timeout = ${request.get_session_timeout() or 'null'};
|
||||
var logout_url = '${request.route_url('logout')}';
|
||||
var noop_url = '${request.route_url('noop')}';
|
||||
% if expose_theme_picker and request.has_perm('common.change_app_theme'):
|
||||
$(function() {
|
||||
$('#theme-picker').change(function() {
|
||||
$(this).parents('form:first').submit();
|
||||
});
|
||||
});
|
||||
% endif
|
||||
</script>
|
||||
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.js') + '?ver={}'.format(tailbone.__version__))}
|
||||
${h.javascript_link(request.static_url('tailbone:static/js/tailbone.feedback.js') + '?ver={}'.format(tailbone.__version__))}
|
||||
${h.javascript_link(request.static_url('tailbone:static/js/jquery.ui.tailbone.js') + '?ver={}'.format(tailbone.__version__))}
|
||||
</%def>
|
||||
|
||||
<%def name="jquery()">
|
||||
${h.javascript_link('https://code.jquery.com/jquery-1.12.4.min.js')}
|
||||
${h.javascript_link('https://code.jquery.com/ui/{}/jquery-ui.min.js'.format(request.rattail_config.get('tailbone', 'jquery_ui.version', default='1.11.4')))}
|
||||
</%def>
|
||||
|
||||
<%def name="extra_javascript()"></%def>
|
||||
|
||||
<%def name="core_styles()">
|
||||
|
||||
${h.stylesheet_link('https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.2/css/bulma.min.css')}
|
||||
|
||||
${self.jquery_theme()}
|
||||
${h.stylesheet_link(request.static_url('tailbone:static/css/jquery.loadmask.css'))}
|
||||
${h.stylesheet_link(request.static_url('tailbone:static/css/jquery.ui.timepicker.css'))}
|
||||
${h.stylesheet_link(request.static_url('tailbone:static/css/jquery.ui.tailbone.css') + '?ver={}'.format(tailbone.__version__))}
|
||||
|
||||
${h.stylesheet_link(request.static_url('tailbone:static/themes/bobcat/css/base.css') + '?ver={}'.format(tailbone.__version__))}
|
||||
${h.stylesheet_link(request.static_url('tailbone:static/themes/bobcat/css/layout.css') + '?ver={}'.format(tailbone.__version__))}
|
||||
${h.stylesheet_link(request.static_url('tailbone:static/css/grids.css') + '?ver={}'.format(tailbone.__version__))}
|
||||
${h.stylesheet_link(request.static_url('tailbone:static/css/filters.css') + '?ver={}'.format(tailbone.__version__))}
|
||||
${h.stylesheet_link(request.static_url('tailbone:static/themes/bobcat/css/forms.css') + '?ver={}'.format(tailbone.__version__))}
|
||||
${h.stylesheet_link(request.static_url('tailbone:static/css/diffs.css') + '?ver={}'.format(tailbone.__version__))}
|
||||
</%def>
|
||||
|
||||
<%def name="jquery_theme()">
|
||||
${h.stylesheet_link('https://code.jquery.com/ui/1.11.4/themes/dark-hive/jquery-ui.css')}
|
||||
</%def>
|
||||
|
||||
<%def name="extra_styles()"></%def>
|
||||
|
||||
<%def name="head_tags()"></%def>
|
||||
|
||||
<%def name="wtfield(form, name, **kwargs)">
|
||||
<div class="field-wrapper${' error' if form[name].errors else ''}">
|
||||
<label for="${name}">${form[name].label}</label>
|
||||
<div class="field">
|
||||
${form[name](**kwargs)}
|
||||
</div>
|
||||
</div>
|
||||
</%def>
|
|
@ -1,231 +0,0 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
## largely copied from https://github.com/dansup/bulma-templates/blob/master/templates/admin.html
|
||||
## <%namespace file="/feedback_dialog.mako" import="feedback_dialog" />
|
||||
<%namespace name="base_meta" file="/base_meta.mako" />
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
## <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>${base_meta.global_title()} » ${capture(self.title)|n}</title>
|
||||
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,700" rel="stylesheet">
|
||||
<!-- Bulma Version 0.7.4-->
|
||||
<link rel="stylesheet" href="https://unpkg.com/bulma@0.7.4/css/bulma.min.css" />
|
||||
${h.stylesheet_link(request.static_url('tailbone:static/themes/dodo/css/admin.css') + '?ver={}'.format(tailbone.__version__))}
|
||||
|
||||
${h.stylesheet_link(request.static_url('tailbone:static/themes/dodo/css/base.css') + '?ver={}'.format(tailbone.__version__))}
|
||||
|
||||
% if background_color:
|
||||
<style type="text/css">
|
||||
html, body {
|
||||
background-color: ${background_color};
|
||||
}
|
||||
</style>
|
||||
% endif
|
||||
|
||||
% if not request.rattail_config.production():
|
||||
<style type="text/css">
|
||||
html, body, body > .navbar {
|
||||
background-image: url(${request.static_url('tailbone:static/img/testing.png')});
|
||||
}
|
||||
</style>
|
||||
% endif
|
||||
|
||||
${h.javascript_link('https://code.jquery.com/jquery-1.12.4.min.js')}
|
||||
<script type="text/javascript">
|
||||
var session_timeout = ${request.get_session_timeout() or 'null'};
|
||||
var logout_url = '${request.route_url('logout')}';
|
||||
var noop_url = '${request.route_url('noop')}';
|
||||
% if expose_theme_picker and request.has_perm('common.change_app_theme'):
|
||||
$(function() {
|
||||
$('#theme-picker').change(function() {
|
||||
$(this).parents('form:first').submit();
|
||||
});
|
||||
});
|
||||
% endif
|
||||
</script>
|
||||
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<!-- START NAV -->
|
||||
<nav class="navbar is-white">
|
||||
<div class="container">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item brand-text" href="${url('home')}">
|
||||
${base_meta.header_logo()}
|
||||
${base_meta.global_title()}
|
||||
</a>
|
||||
|
||||
<div class="navbar-burger burger" data-target="navMenu">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="navMenu" class="navbar-menu">
|
||||
<div class="navbar-start">
|
||||
|
||||
## User Menu
|
||||
% if request.user:
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
% if messaging_enabled:
|
||||
<a class="navbar-link ${'root-user' if request.is_root else ''}">${request.user}${" ({})".format(inbox_count) if inbox_count else ''}</a>
|
||||
% else:
|
||||
<a class="navbar-link ${'root-user' if request.is_root else ''}">${request.user}</a>
|
||||
% endif
|
||||
<div class="navbar-dropdown">
|
||||
% if request.is_root:
|
||||
${h.link_to("Stop being root", url('stop_root'), class_='navbar-item root-user')}
|
||||
% elif request.is_admin:
|
||||
${h.link_to("Become root", url('become_root'), class_='navbar-item root-user')}
|
||||
% endif
|
||||
% if messaging_enabled:
|
||||
${h.link_to("Messages{}".format(" ({})".format(inbox_count) if inbox_count else ''), url('messages.inbox'), class_='navbar-item')}
|
||||
% endif
|
||||
${h.link_to("Change Password", url('change_password'), class_='navbar-item')}
|
||||
${h.link_to("Logout", url('logout'), class_='navbar-item')}
|
||||
</div>
|
||||
</div>
|
||||
% else:
|
||||
${h.link_to("Login", url('login'), class_='navbar-item')}
|
||||
% endif
|
||||
|
||||
</div><!-- navbar-start -->
|
||||
|
||||
<div class="navbar-end">
|
||||
|
||||
## Theme Picker
|
||||
% if expose_theme_picker and request.has_perm('common.change_app_theme'):
|
||||
<div class="level-item">
|
||||
${h.form(url('change_theme'), method="post")}
|
||||
${h.csrf_token(request)}
|
||||
<div class="columns is-vcentered">
|
||||
<div class="column">
|
||||
Theme:
|
||||
</div>
|
||||
<div class="column theme-picker">
|
||||
<div class="select">
|
||||
${h.select('theme', theme, options=theme_picker_options, id='theme-picker')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${h.end_form()}
|
||||
</div>
|
||||
% endif
|
||||
|
||||
</div><!-- navbar-end -->
|
||||
|
||||
</div><!-- navbar-menu -->
|
||||
</div>
|
||||
</nav>
|
||||
<!-- END NAV -->
|
||||
<div class="container">
|
||||
<div class="columns">
|
||||
<div class="column is-3 ">
|
||||
<aside class="menu is-hidden-mobile">
|
||||
|
||||
% for topitem in menus:
|
||||
% if topitem.is_link:
|
||||
${h.link_to(topitem.title, topitem.url, target=topitem.target, class_='navbar-item')}
|
||||
% else:
|
||||
<p class="menu-label">${topitem.title}</p>
|
||||
<ul class="menu-list">
|
||||
% for subitem in topitem.items:
|
||||
% if not subitem.is_sep:
|
||||
<li>${h.link_to(subitem.title, subitem.url, target=subitem.target)}</li>
|
||||
% endif
|
||||
% endfor
|
||||
</ul>
|
||||
% endif
|
||||
% endfor
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
<div class="column is-9">
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
|
||||
## Current Context
|
||||
% if master:
|
||||
% if master.listing:
|
||||
<li>${index_title}</li>
|
||||
% else:
|
||||
<li>${h.link_to(index_title, index_url)}</li>
|
||||
% if parent_url is not Undefined:
|
||||
<li>${h.link_to(parent_title, parent_url)}</li>
|
||||
% elif instance_url is not Undefined:
|
||||
<li>${h.link_to(instance_title, instance_url)}</li>
|
||||
% endif
|
||||
## % if master.viewing and grid_index:
|
||||
## ${grid_index_nav()}
|
||||
## % endif
|
||||
% endif
|
||||
% elif index_title:
|
||||
<li>${index_title}</li>
|
||||
% endif
|
||||
|
||||
% if capture(self.content_title):
|
||||
|
||||
## % if show_prev_next is not Undefined and show_prev_next:
|
||||
## <div style="float: right;">
|
||||
## % if prev_url:
|
||||
## ${h.link_to("« Older", prev_url, class_='button autodisable')}
|
||||
## % else:
|
||||
## ${h.link_to("« Older", '#', class_='button', disabled='disabled')}
|
||||
## % endif
|
||||
## % if next_url:
|
||||
## ${h.link_to("Newer »", next_url, class_='button autodisable')}
|
||||
## % else:
|
||||
## ${h.link_to("Newer »", '#', class_='button', disabled='disabled')}
|
||||
## % endif
|
||||
## </div>
|
||||
## % endif
|
||||
|
||||
<li class="is-active"><a href="#" aria-current="page">${self.content_title()}</a></li>
|
||||
% endif
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
<section id="page-body">
|
||||
|
||||
% if request.session.peek_flash('error'):
|
||||
% for error in request.session.pop_flash('error'):
|
||||
<div class="notification is-warning">
|
||||
<!-- <button class="delete"></button> -->
|
||||
${error}
|
||||
</div>
|
||||
% endfor
|
||||
% endif
|
||||
|
||||
% if request.session.peek_flash():
|
||||
% for msg in request.session.pop_flash():
|
||||
<div class="notification is-info">
|
||||
<!-- <button class="delete"></button> -->
|
||||
${msg}
|
||||
</div>
|
||||
% endfor
|
||||
% endif
|
||||
|
||||
${self.body()}
|
||||
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${h.javascript_link(request.static_url('tailbone:static/themes/dodo/js/bulma.js'), async='true')}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
<%def name="title()"></%def>
|
||||
|
||||
<%def name="content_title()">
|
||||
${self.title()}
|
||||
</%def>
|
|
@ -136,7 +136,6 @@
|
|||
${h.stylesheet_link(request.static_url('tailbone:static/themes/falafel/css/grids.rowstatus.css') + '?ver={}'.format(tailbone.__version__))}
|
||||
## ${h.stylesheet_link(request.static_url('tailbone:static/css/filters.css') + '?ver={}'.format(tailbone.__version__))}
|
||||
${h.stylesheet_link(request.static_url('tailbone:static/themes/falafel/css/filters.css') + '?ver={}'.format(tailbone.__version__))}
|
||||
${h.stylesheet_link(request.static_url('tailbone:static/themes/bobcat/css/forms.css') + '?ver={}'.format(tailbone.__version__))}
|
||||
${h.stylesheet_link(request.static_url('tailbone:static/themes/falafel/css/forms.css') + '?ver={}'.format(tailbone.__version__))}
|
||||
${h.stylesheet_link(request.static_url('tailbone:static/css/diffs.css') + '?ver={}'.format(tailbone.__version__))}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2020 Lance Edgar
|
||||
# Copyright © 2010-2021 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
|
@ -87,12 +87,11 @@ class AuthenticationView(View):
|
|||
self.request.session.flash(msg, allow_duplicate=False)
|
||||
return self.redirect(next_url)
|
||||
|
||||
def login(self, mobile=False):
|
||||
def login(self, **kwargs):
|
||||
"""
|
||||
The login view, responsible for displaying and handling the login form.
|
||||
"""
|
||||
home = 'mobile.home' if mobile else 'home'
|
||||
referrer = self.request.get_referrer(default=self.request.route_url(home))
|
||||
referrer = self.request.get_referrer(default=self.request.route_url('home'))
|
||||
|
||||
# redirect if already logged in
|
||||
if self.request.user:
|
||||
|
@ -138,10 +137,7 @@ class AuthenticationView(View):
|
|||
def authenticate_user(self, username, password):
|
||||
return authenticate_user(Session(), username, password)
|
||||
|
||||
def mobile_login(self):
|
||||
return self.login(mobile=True)
|
||||
|
||||
def logout(self, mobile=False):
|
||||
def logout(self, **kwargs):
|
||||
"""
|
||||
View responsible for logging out the current user.
|
||||
|
||||
|
@ -153,17 +149,12 @@ class AuthenticationView(View):
|
|||
|
||||
# redirect to home page after login, if so configured
|
||||
if self.rattail_config.getbool('tailbone', 'home_after_logout', default=False):
|
||||
home = 'mobile.home' if mobile else 'home'
|
||||
return self.redirect(self.request.route_url(home), headers=headers)
|
||||
return self.redirect(self.request.route_url('home'), headers=headers)
|
||||
|
||||
# otherwise redirect to referrer, with 'login' page as fallback
|
||||
login = 'mobile.login' if mobile else 'login'
|
||||
referrer = self.request.get_referrer(default=self.request.route_url(login))
|
||||
referrer = self.request.get_referrer(default=self.request.route_url('login'))
|
||||
return self.redirect(referrer, headers=headers)
|
||||
|
||||
def mobile_logout(self):
|
||||
return self.logout(mobile=True)
|
||||
|
||||
def noop(self):
|
||||
"""
|
||||
View to serve as "no-op" / ping action to reset current user's session timer
|
||||
|
@ -216,7 +207,6 @@ class AuthenticationView(View):
|
|||
@classmethod
|
||||
def defaults(cls, config):
|
||||
rattail_config = config.registry.settings.get('rattail_config')
|
||||
legacy_mobile = cls.legacy_mobile_enabled(rattail_config)
|
||||
|
||||
# forbidden
|
||||
config.add_forbidden_view(cls, attr='forbidden')
|
||||
|
@ -224,16 +214,10 @@ class AuthenticationView(View):
|
|||
# login
|
||||
config.add_route('login', '/login')
|
||||
config.add_view(cls, attr='login', route_name='login', renderer='/login.mako')
|
||||
if legacy_mobile:
|
||||
config.add_route('mobile.login', '/mobile/login')
|
||||
config.add_view(cls, attr='mobile_login', route_name='mobile.login', renderer='/mobile/login.mako')
|
||||
|
||||
# logout
|
||||
config.add_route('logout', '/logout')
|
||||
config.add_view(cls, attr='logout', route_name='logout')
|
||||
if legacy_mobile:
|
||||
config.add_route('mobile.logout', '/mobile/logout')
|
||||
config.add_view(cls, attr='mobile_logout', route_name='mobile.logout')
|
||||
|
||||
# no-op
|
||||
config.add_route('noop', '/noop')
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2020 Lance Edgar
|
||||
# Copyright © 2010-2021 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
|
@ -83,9 +83,6 @@ class BatchMasterView(MasterView):
|
|||
executable = True
|
||||
results_refreshable = False
|
||||
results_executable = False
|
||||
supports_mobile = True
|
||||
mobile_filterable = True
|
||||
mobile_rows_viewable = True
|
||||
has_worksheet = False
|
||||
has_worksheet_file = False
|
||||
|
||||
|
@ -175,12 +172,6 @@ class BatchMasterView(MasterView):
|
|||
|
||||
kwargs['execute_title'] = self.get_execute_title(batch)
|
||||
kwargs['execute_enabled'] = self.instance_executable(batch)
|
||||
if kwargs['mobile']:
|
||||
if self.mobile_rows_creatable:
|
||||
kwargs.setdefault('add_item_title', "Add Item")
|
||||
if self.mobile_rows_quickable:
|
||||
kwargs.setdefault('quick_entry_placeholder', "Enter {}".format(
|
||||
self.rattail_config.product_key_title()))
|
||||
if kwargs['execute_enabled']:
|
||||
url = self.get_action_url('execute', batch)
|
||||
kwargs['execute_form'] = self.make_execute_form(batch, action_url=url)
|
||||
|
@ -337,18 +328,6 @@ class BatchMasterView(MasterView):
|
|||
return "{} {}".format(batch.id_str, batch.description)
|
||||
return batch.id_str
|
||||
|
||||
def get_mobile_data(self, session=None):
|
||||
return super(BatchMasterView, self).get_mobile_data(session=session)\
|
||||
.order_by(self.model_class.id.desc())
|
||||
|
||||
def make_mobile_filters(self):
|
||||
"""
|
||||
Returns a set of filters for the mobile grid.
|
||||
"""
|
||||
filters = grids.filters.GridFilterSet()
|
||||
filters['status'] = MobileBatchStatusFilter(self.model_class, 'status', default_value='pending')
|
||||
return filters
|
||||
|
||||
def configure_form(self, f):
|
||||
super(BatchMasterView, self).configure_form(f)
|
||||
|
||||
|
@ -488,28 +467,6 @@ class BatchMasterView(MasterView):
|
|||
url = self.request.route_url('users.view', uuid=user.uuid)
|
||||
return tags.link_to(title, url)
|
||||
|
||||
def configure_mobile_form(self, f):
|
||||
super(BatchMasterView, self).configure_mobile_form(f)
|
||||
batch = f.model_instance
|
||||
|
||||
if self.creating:
|
||||
f.remove_fields('id',
|
||||
'rowcount',
|
||||
'created',
|
||||
'created_by',
|
||||
'cognized',
|
||||
'cognized_by',
|
||||
'executed',
|
||||
'executed_by',
|
||||
'purge')
|
||||
|
||||
else: # not creating
|
||||
if not batch.executed:
|
||||
f.remove_fields('executed',
|
||||
'executed_by')
|
||||
if not batch.complete:
|
||||
f.remove_field('complete')
|
||||
|
||||
def save_create_form(self, form):
|
||||
uploads = self.normalize_uploads(form)
|
||||
self.before_create(form)
|
||||
|
@ -547,28 +504,7 @@ class BatchMasterView(MasterView):
|
|||
os.remove(upload['temp_path'])
|
||||
os.rmdir(upload['tempdir'])
|
||||
|
||||
def save_mobile_create_form(self, form):
|
||||
self.before_create(form)
|
||||
session = self.Session()
|
||||
with session.no_autoflush:
|
||||
|
||||
# transfer form data to batch instance
|
||||
batch = self.objectify(form, self.form_deserialized)
|
||||
|
||||
# current user is batch creator
|
||||
batch.created_by = self.request.user
|
||||
|
||||
# TODO: is this still necessary with colander?
|
||||
# destroy initial batch and re-make using handler
|
||||
kwargs = self.get_batch_kwargs(batch)
|
||||
if batch in session:
|
||||
session.expunge(batch)
|
||||
batch = self.handler.make_batch(session, **kwargs)
|
||||
|
||||
session.flush()
|
||||
return batch
|
||||
|
||||
def get_batch_kwargs(self, batch, mobile=False):
|
||||
def get_batch_kwargs(self, batch, **kwargs):
|
||||
"""
|
||||
Return a kwargs dict for use with ``self.handler.make_batch()``, using
|
||||
the given batch as a template.
|
||||
|
@ -599,13 +535,13 @@ class BatchMasterView(MasterView):
|
|||
"""
|
||||
return True
|
||||
|
||||
def redirect_after_create(self, batch, mobile=False):
|
||||
def redirect_after_create(self, batch, **kwargs):
|
||||
if self.handler.should_populate(batch):
|
||||
return self.redirect(self.get_action_url('prefill', batch, mobile=mobile))
|
||||
return self.redirect(self.get_action_url('prefill', batch))
|
||||
elif self.refresh_after_create:
|
||||
return self.redirect(self.get_action_url('refresh', batch, mobile=mobile))
|
||||
return self.redirect(self.get_action_url('refresh', batch))
|
||||
else:
|
||||
return self.redirect(self.get_action_url('view', batch, mobile=mobile))
|
||||
return self.redirect(self.get_action_url('view', batch))
|
||||
|
||||
def template_kwargs_edit(self, **kwargs):
|
||||
batch = kwargs['instance']
|
||||
|
@ -631,16 +567,6 @@ class BatchMasterView(MasterView):
|
|||
def mark_batch_incomplete(self, batch):
|
||||
self.handler.mark_incomplete(batch)
|
||||
|
||||
def mobile_mark_complete(self):
|
||||
batch = self.get_instance()
|
||||
self.mark_batch_complete(batch)
|
||||
return self.redirect(self.get_index_url(mobile=True))
|
||||
|
||||
def mobile_mark_pending(self):
|
||||
batch = self.get_instance()
|
||||
self.mark_batch_incomplete(batch)
|
||||
return self.redirect(self.get_action_url('view', batch, mobile=True))
|
||||
|
||||
def rows_creatable_for(self, batch):
|
||||
"""
|
||||
Only allow creating new rows on a batch if it hasn't yet been executed
|
||||
|
@ -703,16 +629,6 @@ class BatchMasterView(MasterView):
|
|||
return self.redirect(self.get_action_url('view', batch))
|
||||
return super(BatchMasterView, self).create_row()
|
||||
|
||||
def mobile_create_row(self):
|
||||
"""
|
||||
Only allow creating a new row if the batch hasn't yet been executed.
|
||||
"""
|
||||
batch = self.get_instance()
|
||||
if batch.executed:
|
||||
self.request.session.flash("You cannot add new rows to a batch which has been executed")
|
||||
return self.redirect(self.get_action_url('view', batch, mobile=True))
|
||||
return super(BatchMasterView, self).mobile_create_row()
|
||||
|
||||
def save_create_row_form(self, form):
|
||||
batch = self.get_instance()
|
||||
row = self.objectify(form, self.form_deserialized)
|
||||
|
@ -739,19 +655,6 @@ class BatchMasterView(MasterView):
|
|||
# status text
|
||||
f.set_readonly('status_text')
|
||||
|
||||
def configure_mobile_row_form(self, f):
|
||||
super(BatchMasterView, self).configure_mobile_row_form(f)
|
||||
|
||||
# sequence
|
||||
f.set_readonly('sequence')
|
||||
|
||||
# status_code
|
||||
if self.model_row_class:
|
||||
f.set_enum('status_code', self.model_row_class.STATUS)
|
||||
f.set_renderer('status_code', self.render_row_status)
|
||||
f.set_readonly('status_code')
|
||||
f.set_label('status_code', "Status")
|
||||
|
||||
def make_default_row_grid_tools(self, batch):
|
||||
if self.rows_creatable and not batch.executed and not batch.complete:
|
||||
permission_prefix = self.get_permission_prefix()
|
||||
|
@ -803,9 +706,6 @@ class BatchMasterView(MasterView):
|
|||
def make_row_grid_tools(self, batch):
|
||||
return (self.make_default_row_grid_tools(batch) or '') + (self.make_batch_row_grid_tools(batch) or '')
|
||||
|
||||
def sort_mobile_row_data(self, query):
|
||||
return query.order_by(self.model_row_class.sequence)
|
||||
|
||||
def redirect_after_edit(self, batch):
|
||||
"""
|
||||
If refresh flag is set, do that; otherwise go (back) to view/edit page.
|
||||
|
@ -821,12 +721,7 @@ class BatchMasterView(MasterView):
|
|||
self.handler.do_delete(batch)
|
||||
super(BatchMasterView, self).delete_instance(batch)
|
||||
|
||||
def get_fallback_templates(self, template, mobile=False):
|
||||
if mobile:
|
||||
return [
|
||||
'/mobile/batch/{}.mako'.format(template),
|
||||
'/mobile/master/{}.mako'.format(template),
|
||||
]
|
||||
def get_fallback_templates(self, template, **kwargs):
|
||||
return [
|
||||
'/batch/{}.mako'.format(template),
|
||||
'/master/{}.mako'.format(template),
|
||||
|
@ -1374,49 +1269,6 @@ class BatchMasterView(MasterView):
|
|||
self.request.session.flash("Invalid request: {}".format(form.make_deform_form().error), 'error')
|
||||
return self.redirect(self.get_action_url('view', batch))
|
||||
|
||||
def mobile_execute(self):
|
||||
"""
|
||||
Mobile view which can prompt user for execution options if applicable,
|
||||
and/or execute a batch. For now this is done in a "blocking" fashion,
|
||||
i.e. no progress bar.
|
||||
"""
|
||||
batch = self.get_instance()
|
||||
model_title = self.get_model_title()
|
||||
instance_title = self.get_instance_title(batch)
|
||||
view_url = self.get_action_url('view', batch, mobile=True)
|
||||
self.executing = True
|
||||
form = self.make_execute_form(batch)
|
||||
if form.validate(newstyle=True):
|
||||
kwargs = dict(form.validated)
|
||||
|
||||
# cache options to use as defaults next time
|
||||
for key, value in form.validated.items():
|
||||
self.request.session['batch.{}.execute_option.{}'.format(batch.batch_key, key)] = value
|
||||
|
||||
try:
|
||||
result = self.handler.execute(batch, user=self.request.user, **kwargs)
|
||||
except Exception as err:
|
||||
log.exception("failed to execute %s %s", model_title, batch.id_str)
|
||||
self.request.session.flash(self.execute_error_message(err), 'error')
|
||||
else:
|
||||
if result:
|
||||
batch.executed = datetime.datetime.utcnow()
|
||||
batch.executed_by = self.request.user
|
||||
self.request.session.flash("{} was executed: {}".format(model_title, instance_title))
|
||||
else:
|
||||
log.error("not sure why, but failed to execute %s %s: %s", model_title, batch.id_str, batch)
|
||||
self.request.session.flash("Failed to execute {}: {}".format(model_title, err), 'error')
|
||||
return self.redirect(view_url)
|
||||
|
||||
form.mobile = True
|
||||
form.submit_label = "Execute"
|
||||
form.cancel_url = view_url
|
||||
return self.render_to_response('execute', {
|
||||
'form': form,
|
||||
'instance_title': instance_title,
|
||||
'instance_url': view_url,
|
||||
}, mobile=True)
|
||||
|
||||
def execute_error_message(self, error):
|
||||
return "Batch execution failed: {}".format(simple_error(error))
|
||||
|
||||
|
@ -1576,7 +1428,6 @@ class BatchMasterView(MasterView):
|
|||
permission_prefix = cls.get_permission_prefix()
|
||||
model_title = cls.get_model_title()
|
||||
model_title_plural = cls.get_model_title_plural()
|
||||
legacy_mobile = cls.legacy_mobile_enabled(rattail_config)
|
||||
|
||||
# TODO: currently must do this here (in addition to `_defaults()` or
|
||||
# else the perm group label will not display correctly...
|
||||
|
@ -1635,18 +1486,6 @@ class BatchMasterView(MasterView):
|
|||
config.add_view(cls, attr='toggle_complete', route_name='{}.toggle_complete'.format(route_prefix),
|
||||
permission='{}.edit'.format(permission_prefix))
|
||||
|
||||
# mobile mark complete
|
||||
if legacy_mobile:
|
||||
config.add_route('mobile.{}.mark_complete'.format(route_prefix), '/mobile{}/{{{}}}/mark-complete'.format(url_prefix, model_key))
|
||||
config.add_view(cls, attr='mobile_mark_complete', route_name='mobile.{}.mark_complete'.format(route_prefix),
|
||||
permission='{}.edit'.format(permission_prefix))
|
||||
|
||||
# mobile mark pending
|
||||
if legacy_mobile:
|
||||
config.add_route('mobile.{}.mark_pending'.format(route_prefix), '/mobile{}/{{{}}}/mark-pending'.format(url_prefix, model_key))
|
||||
config.add_view(cls, attr='mobile_mark_pending', route_name='mobile.{}.mark_pending'.format(route_prefix),
|
||||
permission='{}.edit'.format(permission_prefix))
|
||||
|
||||
# refresh multiple batches (results)
|
||||
if cls.results_refreshable:
|
||||
config.add_route('{}.refresh_results'.format(route_prefix), '{}/refresh-results'.format(url_prefix),
|
||||
|
@ -1709,33 +1548,3 @@ class UploadWorksheet(colander.Schema):
|
|||
class ToggleComplete(colander.MappingSchema):
|
||||
|
||||
complete = colander.SchemaNode(colander.Boolean())
|
||||
|
||||
|
||||
class MobileBatchStatusFilter(grids.filters.MobileFilter):
|
||||
|
||||
value_choices = ['pending', 'complete', 'executed', 'all']
|
||||
|
||||
def __init__(self, model_class, key, **kwargs):
|
||||
self.model_class = model_class
|
||||
super(MobileBatchStatusFilter, self).__init__(key, **kwargs)
|
||||
|
||||
def filter_equal(self, query, value):
|
||||
|
||||
if value == 'pending':
|
||||
return query.filter(self.model_class.executed == None)\
|
||||
.filter(sa.or_(
|
||||
self.model_class.complete == None,
|
||||
self.model_class.complete == False))
|
||||
|
||||
if value == 'complete':
|
||||
return query.filter(self.model_class.executed == None)\
|
||||
.filter(self.model_class.complete == True)
|
||||
|
||||
if value == 'executed':
|
||||
return query.filter(self.model_class.executed != None)
|
||||
|
||||
return query
|
||||
|
||||
def iter_choices(self):
|
||||
for value in self.value_choices:
|
||||
yield value, prettify(value)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2020 Lance Edgar
|
||||
# Copyright © 2010-2021 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
|
@ -35,7 +35,6 @@ import six
|
|||
from rattail import pod
|
||||
from rattail.db import model
|
||||
from rattail.db.util import make_full_description
|
||||
from rattail.time import localtime
|
||||
from rattail.gpc import GPC
|
||||
from rattail.util import pretty_quantity, OrderedDict
|
||||
|
||||
|
@ -63,8 +62,6 @@ class InventoryBatchView(BatchMasterView):
|
|||
index_title = "Inventory"
|
||||
rows_creatable = True
|
||||
bulk_deletable = True
|
||||
mobile_creatable = True
|
||||
mobile_rows_creatable = True
|
||||
|
||||
# set to True for the UI to "prefer" case amounts, as opposed to unit
|
||||
prefer_cases = False
|
||||
|
@ -101,15 +98,6 @@ class InventoryBatchView(BatchMasterView):
|
|||
'executed_by',
|
||||
]
|
||||
|
||||
mobile_form_fields = [
|
||||
'mode',
|
||||
'reason_code',
|
||||
'rowcount',
|
||||
'complete',
|
||||
'executed',
|
||||
'executed_by',
|
||||
]
|
||||
|
||||
model_row_class = model.InventoryBatchRow
|
||||
rows_editable = True
|
||||
|
||||
|
@ -160,13 +148,6 @@ class InventoryBatchView(BatchMasterView):
|
|||
# total_cost
|
||||
g.set_type('total_cost', 'currency')
|
||||
|
||||
def render_mobile_listitem(self, batch, i):
|
||||
return "({}) {} rows - {}, {}".format(
|
||||
batch.id_str,
|
||||
"?" if batch.rowcount is None else batch.rowcount,
|
||||
batch.created_by,
|
||||
localtime(self.request.rattail_config, batch.created, from_utc=True).strftime('%Y-%m-%d'))
|
||||
|
||||
def mutable_batch(self, batch):
|
||||
return not batch.executed and not batch.complete and batch.mode != self.enum.INVENTORY_MODE_ZERO_ALL
|
||||
|
||||
|
@ -397,56 +378,6 @@ class InventoryBatchView(BatchMasterView):
|
|||
data['image_url'] = pod.get_image_url(self.rattail_config, product.upc)
|
||||
return data
|
||||
|
||||
def configure_mobile_form(self, f):
|
||||
super(InventoryBatchView, self).configure_mobile_form(f)
|
||||
batch = f.model_instance
|
||||
|
||||
# mode
|
||||
modes = self.get_available_modes()
|
||||
f.set_enum('mode', modes)
|
||||
mode_values = [(k, v) for k, v in sorted(modes.items())]
|
||||
f.set_widget('mode', forms.widgets.PlainSelectWidget(values=mode_values))
|
||||
|
||||
# complete
|
||||
if self.creating or batch.executed or not batch.complete:
|
||||
f.remove_field('complete')
|
||||
|
||||
# rowcount
|
||||
if self.viewing and not batch.executed and not batch.complete:
|
||||
f.remove_field('rowcount')
|
||||
|
||||
# TODO: this view can create new rows, with only a GET query. that should
|
||||
# probably be changed to require POST; for now we just require the "create
|
||||
# batch row" perm and call it good..
|
||||
def mobile_row_from_upc(self):
|
||||
"""
|
||||
Locate and/or create a row within the batch, according to the given
|
||||
product UPC, then redirect to the row view page.
|
||||
"""
|
||||
batch = self.get_instance()
|
||||
row = None
|
||||
raw_entry = self.request.GET.get('upc', '')
|
||||
entry = raw_entry.strip()
|
||||
entry = re.sub(r'\D', '', entry)
|
||||
if entry:
|
||||
|
||||
if len(entry) <= 14:
|
||||
row = self.add_row_for_upc(batch, entry, warn_if_present=True)
|
||||
if not row:
|
||||
self.request.session.flash("Product not found: {}".format(entry), 'error')
|
||||
return self.redirect(self.get_action_url('view', batch, mobile=True))
|
||||
|
||||
else:
|
||||
self.request.session.flash("UPC has too many digits ({}): {}".format(len(entry), entry), 'error')
|
||||
return self.redirect(self.get_action_url('view', batch, mobile=True))
|
||||
|
||||
else:
|
||||
self.request.session.flash("Product not found: {}".format(raw_entry), 'error')
|
||||
return self.redirect(self.get_action_url('view', batch, mobile=True))
|
||||
|
||||
self.Session.flush()
|
||||
return self.redirect(self.mobile_row_route_url('view', uuid=row.batch_uuid, row_uuid=row.uuid))
|
||||
|
||||
def add_row_for_upc(self, batch, entry, warn_if_present=False):
|
||||
"""
|
||||
Add a row to the batch for the given UPC, if applicable.
|
||||
|
@ -467,76 +398,13 @@ class InventoryBatchView(BatchMasterView):
|
|||
kwargs['product_image_url'] = pod.get_image_url(self.rattail_config, row.upc)
|
||||
return kwargs
|
||||
|
||||
def get_batch_kwargs(self, batch, mobile=False):
|
||||
kwargs = super(InventoryBatchView, self).get_batch_kwargs(batch, mobile=False)
|
||||
def get_batch_kwargs(self, batch, **kwargs):
|
||||
kwargs = super(InventoryBatchView, self).get_batch_kwargs(batch, **kwargs)
|
||||
kwargs['mode'] = batch.mode
|
||||
kwargs['complete'] = False
|
||||
kwargs['reason_code'] = batch.reason_code
|
||||
return kwargs
|
||||
|
||||
def get_mobile_row_data(self, batch):
|
||||
# we want newest on top, for inventory batch rows
|
||||
return self.get_row_data(batch)\
|
||||
.order_by(self.model_row_class.sequence.desc())
|
||||
|
||||
# TODO: ugh, the hackiness. needs a refactor fo sho
|
||||
def mobile_view_row(self):
|
||||
"""
|
||||
Mobile view for inventory batch rows. Note that this also handles
|
||||
updating a row...ugh.
|
||||
"""
|
||||
self.viewing = True
|
||||
row = self.get_row_instance()
|
||||
batch = self.get_parent(row)
|
||||
form = self.make_mobile_row_form(row)
|
||||
|
||||
allow_cases = self.allow_cases(batch)
|
||||
unit_uom = 'LB' if row.product and row.product.weighed else 'EA'
|
||||
if row.cases and allow_cases:
|
||||
uom = 'CS'
|
||||
elif row.units:
|
||||
uom = unit_uom
|
||||
elif row.case_quantity and allow_cases and self.prefer_cases:
|
||||
uom = 'CS'
|
||||
else:
|
||||
uom = unit_uom
|
||||
|
||||
context = {
|
||||
'row': row,
|
||||
'batch': batch,
|
||||
'instance': row,
|
||||
'instance_title': self.get_row_instance_title(row),
|
||||
'parent_model_title': self.get_model_title(),
|
||||
'parent_title': self.get_instance_title(batch),
|
||||
'parent_url': self.get_action_url('view', batch, mobile=True),
|
||||
'product_image_url': pod.get_image_url(self.rattail_config, row.upc),
|
||||
'form': form,
|
||||
'allow_cases': allow_cases,
|
||||
'unit_uom': unit_uom,
|
||||
'uom': uom,
|
||||
}
|
||||
|
||||
if self.request.has_perm('{}.edit_row'.format(self.get_permission_prefix())):
|
||||
schema = InventoryForm().bind(session=self.Session())
|
||||
update_form = forms.Form(schema=schema, request=self.request)
|
||||
if update_form.validate(newstyle=True):
|
||||
row = self.Session.query(model.InventoryBatchRow).get(update_form.validated['row'])
|
||||
cases = update_form.validated['cases']
|
||||
units = update_form.validated['units']
|
||||
if cases is not colander.null:
|
||||
row.cases = cases
|
||||
row.units = None
|
||||
elif units is not colander.null:
|
||||
row.cases = None
|
||||
row.units = units
|
||||
else:
|
||||
raise NotImplementedError
|
||||
self.handler.refresh_row(row)
|
||||
route_prefix = self.get_route_prefix()
|
||||
return self.redirect(self.request.route_url('mobile.{}.view'.format(route_prefix), uuid=batch.uuid))
|
||||
|
||||
return self.render_to_response('view_row', context, mobile=True)
|
||||
|
||||
def get_row_instance_title(self, row):
|
||||
if row.upc:
|
||||
return row.upc.pretty()
|
||||
|
@ -569,12 +437,6 @@ class InventoryBatchView(BatchMasterView):
|
|||
if row.status_code == row.STATUS_PRODUCT_NOT_FOUND:
|
||||
return 'warning'
|
||||
|
||||
def render_mobile_row_listitem(self, row, i):
|
||||
description = row.product.full_description if row.product else row.description
|
||||
unit_uom = 'LB' if row.product and row.product.weighed else 'EA'
|
||||
qty = "{} {}".format(pretty_quantity(row.cases or row.units), 'CS' if row.cases else unit_uom)
|
||||
return "({}) {} - {}".format(row.upc.pretty(), description, qty)
|
||||
|
||||
def configure_row_form(self, f):
|
||||
super(InventoryBatchView, self).configure_row_form(f)
|
||||
row = f.model_instance
|
||||
|
@ -633,7 +495,6 @@ class InventoryBatchView(BatchMasterView):
|
|||
route_prefix = cls.get_route_prefix()
|
||||
url_prefix = cls.get_url_prefix()
|
||||
permission_prefix = cls.get_permission_prefix()
|
||||
legacy_mobile = cls.legacy_mobile_enabled(rattail_config)
|
||||
|
||||
# we need batch handler to determine available permissions
|
||||
factory = cls.get_handler_factory(rattail_config)
|
||||
|
@ -654,38 +515,6 @@ class InventoryBatchView(BatchMasterView):
|
|||
config.add_view(cls, attr='desktop_lookup', route_name='{}.desktop_lookup'.format(route_prefix),
|
||||
renderer='json', permission='{}.create_row'.format(permission_prefix))
|
||||
|
||||
# mobile - make new row from UPC
|
||||
if legacy_mobile:
|
||||
config.add_route('mobile.{}.row_from_upc'.format(route_prefix), '/mobile{}/{{{}}}/row-from-upc'.format(url_prefix, model_key))
|
||||
config.add_view(cls, attr='mobile_row_from_upc', route_name='mobile.{}.row_from_upc'.format(route_prefix),
|
||||
permission='{}.create_row'.format(permission_prefix))
|
||||
|
||||
|
||||
# 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_inventory_batch_row(node, kw):
|
||||
session = kw['session']
|
||||
def validate(node, value):
|
||||
row = session.query(model.InventoryBatchRow).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 InventoryForm(colander.MappingSchema):
|
||||
|
||||
row = colander.SchemaNode(colander.String(),
|
||||
validator=valid_inventory_batch_row)
|
||||
|
||||
cases = colander.SchemaNode(colander.Decimal(), missing=colander.null)
|
||||
|
||||
units = colander.SchemaNode(colander.Decimal(), missing=colander.null)
|
||||
|
||||
|
||||
# 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
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2020 Lance Edgar
|
||||
# Copyright © 2010-2021 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
|
@ -171,8 +171,8 @@ class PricingBatchView(BatchMasterView):
|
|||
if self.request.POST.get('auto_generate_from_srp_breach') == 'true':
|
||||
f.set_required('input_filename', False)
|
||||
|
||||
def get_batch_kwargs(self, batch, mobile=False):
|
||||
kwargs = super(PricingBatchView, self).get_batch_kwargs(batch, mobile=mobile)
|
||||
def get_batch_kwargs(self, batch, **kwargs):
|
||||
kwargs = super(PricingBatchView, self).get_batch_kwargs(batch, **kwargs)
|
||||
kwargs['min_diff_threshold'] = batch.min_diff_threshold
|
||||
kwargs['min_diff_percent'] = batch.min_diff_percent
|
||||
kwargs['calculate_for_manual'] = batch.calculate_for_manual
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2020 Lance Edgar
|
||||
# Copyright © 2010-2021 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
|
@ -55,11 +55,11 @@ class CommonView(View):
|
|||
project_version = tailbone.__version__
|
||||
robots_txt_path = resource_path('tailbone.static:robots.txt')
|
||||
|
||||
def home(self, mobile=False):
|
||||
def home(self, **kwargs):
|
||||
"""
|
||||
Home page view.
|
||||
"""
|
||||
if not mobile and not self.request.user:
|
||||
if not self.request.user:
|
||||
if self.rattail_config.getbool('tailbone', 'login_is_home', default=True):
|
||||
raise self.redirect(self.request.route_url('login'))
|
||||
|
||||
|
@ -96,12 +96,6 @@ class CommonView(View):
|
|||
response.content_type = b'text/plain'
|
||||
return response
|
||||
|
||||
def mobile_home(self):
|
||||
"""
|
||||
Home page view for mobile.
|
||||
"""
|
||||
return self.home(mobile=True)
|
||||
|
||||
def exception(self):
|
||||
"""
|
||||
Generic exception view
|
||||
|
@ -179,12 +173,6 @@ class CommonView(View):
|
|||
return {'ok': True}
|
||||
return {'error': "Form did not validate!"}
|
||||
|
||||
def mobile_feedback(self):
|
||||
"""
|
||||
Generic view to handle the user feedback form on mobile.
|
||||
"""
|
||||
return self.feedback()
|
||||
|
||||
def consume_batch_id(self):
|
||||
"""
|
||||
Consume next batch ID from the PG sequence, and display via flash message.
|
||||
|
@ -207,7 +195,6 @@ class CommonView(View):
|
|||
@classmethod
|
||||
def _defaults(cls, config):
|
||||
rattail_config = config.registry.settings.get('rattail_config')
|
||||
legacy_mobile = cls.legacy_mobile_enabled(rattail_config)
|
||||
|
||||
# auto-correct URLs which require trailing slash
|
||||
config.add_notfound_view(cls, attr='notfound', append_slash=True)
|
||||
|
@ -222,9 +209,6 @@ class CommonView(View):
|
|||
# home
|
||||
config.add_route('home', '/')
|
||||
config.add_view(cls, attr='home', route_name='home', renderer='/home.mako')
|
||||
if legacy_mobile:
|
||||
config.add_route('mobile.home', '/mobile/')
|
||||
config.add_view(cls, attr='mobile_home', route_name='mobile.home', renderer='/mobile/home.mako')
|
||||
|
||||
# robots.txt
|
||||
config.add_route('robots.txt', '/robots.txt')
|
||||
|
@ -233,9 +217,6 @@ class CommonView(View):
|
|||
# about
|
||||
config.add_route('about', '/about')
|
||||
config.add_view(cls, attr='about', route_name='about', renderer='/about.mako')
|
||||
if legacy_mobile:
|
||||
config.add_route('mobile.about', '/mobile/about')
|
||||
config.add_view(cls, attr='about', route_name='mobile.about', renderer='/mobile/about.mako')
|
||||
|
||||
# change db engine
|
||||
config.add_tailbone_permission('common', 'common.change_db_engine',
|
||||
|
@ -255,10 +236,6 @@ class CommonView(View):
|
|||
config.add_route('feedback', '/feedback', request_method='POST')
|
||||
config.add_view(cls, attr='feedback', route_name='feedback',
|
||||
renderer='json', permission='common.feedback')
|
||||
if legacy_mobile:
|
||||
config.add_route('mobile.feedback', '/mobile/feedback', request_method='POST')
|
||||
config.add_view(cls, attr='mobile_feedback', route_name='mobile.feedback',
|
||||
renderer='json', permission='common.feedback')
|
||||
|
||||
# consume batch ID
|
||||
config.add_tailbone_permission('common', 'common.consume_batch_id',
|
||||
|
|
|
@ -42,7 +42,7 @@ from tailbone.db import Session
|
|||
from tailbone.auth import logout_user
|
||||
from tailbone.progress import SessionProgress
|
||||
from tailbone.util import should_use_buefy
|
||||
from tailbone.config import legacy_mobile_enabled, protected_usernames
|
||||
from tailbone.config import protected_usernames
|
||||
|
||||
|
||||
class View(object):
|
||||
|
@ -101,14 +101,6 @@ class View(object):
|
|||
"""
|
||||
return should_use_buefy(self.request)
|
||||
|
||||
@classmethod
|
||||
def legacy_mobile_enabled(cls, rattail_config):
|
||||
"""
|
||||
Returns the boolean setting indicating whether the old / "legacy"
|
||||
(jQuery) mobile app/site should be exposed.
|
||||
"""
|
||||
return legacy_mobile_enabled(rattail_config)
|
||||
|
||||
def late_login_user(self):
|
||||
"""
|
||||
Returns the :class:`rattail:rattail.db.model.User` instance
|
||||
|
|
|
@ -50,7 +50,6 @@ class CustomerView(MasterView):
|
|||
model_class = model.Customer
|
||||
is_contact = True
|
||||
has_versions = True
|
||||
supports_mobile = True
|
||||
people_detachable = True
|
||||
touchable = True
|
||||
|
||||
|
@ -95,20 +94,6 @@ class CustomerView(MasterView):
|
|||
'members',
|
||||
]
|
||||
|
||||
mobile_form_fields = [
|
||||
'id',
|
||||
'name',
|
||||
'default_phone',
|
||||
'default_email',
|
||||
'default_address',
|
||||
'email_preference',
|
||||
'wholesale',
|
||||
'active_in_pos',
|
||||
'active_in_pos_sticky',
|
||||
'people',
|
||||
'groups',
|
||||
]
|
||||
|
||||
def configure_grid(self, g):
|
||||
super(CustomerView, self).configure_grid(g)
|
||||
|
||||
|
@ -154,10 +139,6 @@ class CustomerView(MasterView):
|
|||
g.set_link('person')
|
||||
g.set_link('email')
|
||||
|
||||
def get_mobile_data(self, session=None):
|
||||
# TODO: hacky!
|
||||
return self.get_data(session=session).order_by(model.Customer.name)
|
||||
|
||||
def get_instance(self):
|
||||
try:
|
||||
instance = super(CustomerView, self).get_instance()
|
||||
|
@ -303,8 +284,7 @@ class CustomerView(MasterView):
|
|||
items = []
|
||||
for person in people:
|
||||
text = six.text_type(person)
|
||||
route = '{}people.view'.format('mobile.' if self.mobile else '')
|
||||
url = self.request.route_url(route, uuid=person.uuid)
|
||||
url = self.request.route_url('people.view', uuid=person.uuid)
|
||||
link = tags.link_to(text, url)
|
||||
items.append(HTML.tag('li', c=[link]))
|
||||
return HTML.tag('ul', c=items)
|
||||
|
|
|
@ -89,27 +89,17 @@ class DataSyncChangeView(MasterView):
|
|||
self.request.session.flash("DataSync daemon could not be restarted; result was: {}".format(result), 'error')
|
||||
return self.redirect(self.request.get_referrer(default=self.request.route_url('datasyncchanges')))
|
||||
|
||||
def mobile_index(self):
|
||||
return {}
|
||||
|
||||
@classmethod
|
||||
def defaults(cls, config):
|
||||
rattail_config = config.registry.settings.get('rattail_config')
|
||||
legacy_mobile = cls.legacy_mobile_enabled(rattail_config)
|
||||
|
||||
# fix permission group title
|
||||
config.add_tailbone_permission_group('datasync', label="DataSync")
|
||||
|
||||
# restart datasync
|
||||
config.add_tailbone_permission('datasync', 'datasync.restart', label="Restart DataSync Daemon")
|
||||
# desktop
|
||||
config.add_route('datasync.restart', '/datasync/restart')
|
||||
config.add_view(cls, attr='restart', route_name='datasync.restart', permission='datasync.restart')
|
||||
# mobile
|
||||
if legacy_mobile:
|
||||
config.add_route('datasync.mobile', '/mobile/datasync/')
|
||||
config.add_view(cls, attr='mobile_index', route_name='datasync.mobile',
|
||||
permission='datasync.restart', renderer='/mobile/datasync.mako')
|
||||
|
||||
cls._defaults(config)
|
||||
|
||||
|
|
|
@ -115,14 +115,6 @@ class MasterView(View):
|
|||
# set to True to declare model as "contact"
|
||||
is_contact = False
|
||||
|
||||
supports_mobile = False
|
||||
mobile_creatable = False
|
||||
mobile_editable = False
|
||||
mobile_pageable = True
|
||||
mobile_filterable = False
|
||||
mobile_executable = False
|
||||
|
||||
mobile = False
|
||||
listing = False
|
||||
creating = False
|
||||
creates_multiple = False
|
||||
|
@ -170,14 +162,6 @@ class MasterView(View):
|
|||
rows_downloadable_csv = False
|
||||
rows_downloadable_xlsx = False
|
||||
|
||||
mobile_rows_creatable = False
|
||||
mobile_rows_creatable_via_browse = False
|
||||
mobile_rows_quickable = False
|
||||
mobile_rows_filterable = False
|
||||
mobile_rows_viewable = False
|
||||
mobile_rows_editable = False
|
||||
mobile_rows_deletable = False
|
||||
|
||||
row_labels = {}
|
||||
|
||||
@property
|
||||
|
@ -236,24 +220,6 @@ class MasterView(View):
|
|||
"""
|
||||
return getattr(cls, 'version_grid_factory', grids.Grid)
|
||||
|
||||
@classmethod
|
||||
def get_mobile_grid_factory(cls):
|
||||
"""
|
||||
Must return a callable to be used when creating new mobile grid
|
||||
instances. Instead of overriding this, you can set
|
||||
:attr:`mobile_grid_factory`. Default factory is :class:`MobileGrid`.
|
||||
"""
|
||||
return getattr(cls, 'mobile_grid_factory', grids.MobileGrid)
|
||||
|
||||
@classmethod
|
||||
def get_mobile_row_grid_factory(cls):
|
||||
"""
|
||||
Must return a callable to be used when creating new mobile row grid
|
||||
instances. Instead of overriding this, you can set
|
||||
:attr:`mobile_row_grid_factory`. Default factory is :class:`MobileGrid`.
|
||||
"""
|
||||
return getattr(cls, 'mobile_row_grid_factory', grids.MobileGrid)
|
||||
|
||||
def set_labels(self, obj):
|
||||
labels = self.collect_labels()
|
||||
for key, label in six.iteritems(labels):
|
||||
|
@ -624,163 +590,6 @@ class MasterView(View):
|
|||
def render_version_comment(self, transaction, column):
|
||||
return transaction.meta.get('comment', "")
|
||||
|
||||
def mobile_index(self):
|
||||
"""
|
||||
Mobile "home" page for the data model
|
||||
"""
|
||||
self.mobile = True
|
||||
self.listing = True
|
||||
grid = self.make_mobile_grid()
|
||||
return self.render_to_response('index', {'grid': grid}, mobile=True)
|
||||
|
||||
@classmethod
|
||||
def get_mobile_grid_key(cls):
|
||||
"""
|
||||
Must return a unique "config key" for the mobile grid, for sort/filter
|
||||
purposes etc. (It need only be unique among *mobile* grids.) Instead
|
||||
of overriding this, you can set :attr:`mobile_grid_key`. Default is
|
||||
the value returned by :meth:`get_route_prefix()`.
|
||||
"""
|
||||
if hasattr(cls, 'mobile_grid_key'):
|
||||
return cls.mobile_grid_key
|
||||
return 'mobile.{}'.format(cls.get_route_prefix())
|
||||
|
||||
def make_mobile_grid(self, factory=None, key=None, data=None, columns=None, **kwargs):
|
||||
"""
|
||||
Creates a new mobile grid instance
|
||||
"""
|
||||
if factory is None:
|
||||
factory = self.get_mobile_grid_factory()
|
||||
if key is None:
|
||||
key = self.get_mobile_grid_key()
|
||||
if data is None:
|
||||
data = self.get_mobile_data(session=kwargs.get('session'))
|
||||
if columns is None:
|
||||
columns = self.get_mobile_grid_columns()
|
||||
|
||||
kwargs.setdefault('request', self.request)
|
||||
kwargs.setdefault('mobile', True)
|
||||
kwargs = self.make_mobile_grid_kwargs(**kwargs)
|
||||
grid = factory(key, data, columns, **kwargs)
|
||||
self.configure_mobile_grid(grid)
|
||||
grid.load_settings()
|
||||
return grid
|
||||
|
||||
def get_mobile_grid_columns(self):
|
||||
if hasattr(self, 'mobile_grid_columns'):
|
||||
return self.mobile_grid_columns
|
||||
# TODO
|
||||
return ['listitem']
|
||||
|
||||
def get_mobile_data(self, session=None):
|
||||
"""
|
||||
Must return the "raw" / full data set for the mobile grid. This data
|
||||
should *not* yet be sorted or filtered in any way; that happens later.
|
||||
Default is the value returned by :meth:`get_data()`, in which case all
|
||||
records visible in the traditional view, are visible in mobile too.
|
||||
"""
|
||||
return self.get_data(session=session)
|
||||
|
||||
def make_mobile_grid_kwargs(self, **kwargs):
|
||||
"""
|
||||
Must return a dictionary of kwargs to be passed to the factory when
|
||||
creating new mobile grid instances.
|
||||
"""
|
||||
defaults = {
|
||||
'model_class': getattr(self, 'model_class', None),
|
||||
'pageable': self.mobile_pageable,
|
||||
'sortable': False,
|
||||
'filterable': self.mobile_filterable,
|
||||
'renderers': self.make_mobile_grid_renderers(),
|
||||
'url': lambda obj: self.get_action_url('view', obj, mobile=True),
|
||||
}
|
||||
# TODO: this seems wrong..
|
||||
if self.mobile_filterable:
|
||||
defaults['filters'] = self.make_mobile_filters()
|
||||
defaults.update(kwargs)
|
||||
return defaults
|
||||
|
||||
def make_mobile_grid_renderers(self):
|
||||
return {
|
||||
'listitem': self.render_mobile_listitem,
|
||||
}
|
||||
|
||||
def render_mobile_listitem(self, obj, i):
|
||||
return obj
|
||||
|
||||
def configure_mobile_grid(self, grid):
|
||||
pass
|
||||
|
||||
def make_mobile_row_grid(self, factory=None, key=None, data=None, columns=None, **kwargs):
|
||||
"""
|
||||
Make a new (configured) rows grid instance for mobile.
|
||||
"""
|
||||
instance = kwargs.pop('instance', self.get_instance())
|
||||
|
||||
if factory is None:
|
||||
factory = self.get_mobile_row_grid_factory()
|
||||
if key is None:
|
||||
key = 'mobile.{}.{}'.format(self.get_grid_key(), self.request.matchdict[self.get_model_key()])
|
||||
if data is None:
|
||||
data = self.get_mobile_row_data(instance)
|
||||
if columns is None:
|
||||
columns = self.get_mobile_row_grid_columns()
|
||||
|
||||
kwargs.setdefault('request', self.request)
|
||||
kwargs.setdefault('mobile', True)
|
||||
kwargs = self.make_mobile_row_grid_kwargs(**kwargs)
|
||||
grid = factory(key, data, columns, **kwargs)
|
||||
self.configure_mobile_row_grid(grid)
|
||||
grid.load_settings()
|
||||
return grid
|
||||
|
||||
def get_mobile_row_grid_columns(self):
|
||||
if hasattr(self, 'mobile_row_grid_columns'):
|
||||
return self.mobile_row_grid_columns
|
||||
# TODO
|
||||
return ['listitem']
|
||||
|
||||
def make_mobile_row_grid_kwargs(self, **kwargs):
|
||||
"""
|
||||
Must return a dictionary of kwargs to be passed to the factory when
|
||||
creating new mobile *row* grid instances.
|
||||
"""
|
||||
defaults = {
|
||||
'model_class': self.model_row_class,
|
||||
# TODO
|
||||
'pageable': self.pageable,
|
||||
'sortable': False,
|
||||
'filterable': self.mobile_rows_filterable,
|
||||
'renderers': self.make_mobile_row_grid_renderers(),
|
||||
'url': lambda obj: self.get_row_action_url('view', obj, mobile=True),
|
||||
}
|
||||
# TODO: this seems wrong..
|
||||
if self.mobile_rows_filterable:
|
||||
defaults['filters'] = self.make_mobile_row_filters()
|
||||
defaults.update(kwargs)
|
||||
return defaults
|
||||
|
||||
def make_mobile_row_grid_renderers(self):
|
||||
return {
|
||||
'listitem': self.render_mobile_row_listitem,
|
||||
}
|
||||
|
||||
def configure_mobile_row_grid(self, grid):
|
||||
pass
|
||||
|
||||
def make_mobile_filters(self):
|
||||
"""
|
||||
Returns a set of filters for the mobile grid, if applicable.
|
||||
"""
|
||||
|
||||
def make_mobile_row_filters(self):
|
||||
"""
|
||||
Returns a set of filters for the mobile row grid, if applicable.
|
||||
"""
|
||||
|
||||
def render_mobile_row_listitem(self, obj, i):
|
||||
return obj
|
||||
|
||||
def create(self, form=None, template='create'):
|
||||
"""
|
||||
View for creating a new model record.
|
||||
|
@ -800,22 +609,6 @@ class MasterView(View):
|
|||
context['dform'] = form.make_deform_form()
|
||||
return self.render_to_response(template, context)
|
||||
|
||||
def mobile_create(self):
|
||||
"""
|
||||
Mobile view for creating a new primary object
|
||||
"""
|
||||
self.mobile = True
|
||||
self.creating = True
|
||||
form = self.make_mobile_form(self.get_model_class())
|
||||
if self.request.method == 'POST':
|
||||
if self.validate_mobile_form(form):
|
||||
# let save_create_form() return alternate object if necessary
|
||||
obj = self.save_mobile_create_form(form)
|
||||
self.after_create(obj)
|
||||
self.flash_after_create(obj)
|
||||
return self.redirect_after_create(obj, mobile=True)
|
||||
return self.render_to_response('create', {'form': form}, mobile=True)
|
||||
|
||||
def save_create_form(self, form):
|
||||
uploads = self.normalize_uploads(form)
|
||||
self.before_create(form)
|
||||
|
@ -1044,19 +837,10 @@ class MasterView(View):
|
|||
self.request.session.flash("{} has been created: {}".format(
|
||||
self.get_model_title(), self.get_instance_title(obj)))
|
||||
|
||||
def save_mobile_create_form(self, form):
|
||||
self.before_create(form)
|
||||
with self.Session.no_autoflush:
|
||||
obj = self.objectify(form, self.form_deserialized)
|
||||
self.before_create_flush(obj, form)
|
||||
self.Session.add(obj)
|
||||
self.Session.flush()
|
||||
return obj
|
||||
|
||||
def redirect_after_create(self, instance, mobile=False):
|
||||
def redirect_after_create(self, instance, **kwargs):
|
||||
if self.populatable and self.should_populate(instance):
|
||||
return self.redirect(self.get_action_url('populate', instance, mobile=mobile))
|
||||
return self.redirect(self.get_action_url('view', instance, mobile=mobile))
|
||||
return self.redirect(self.get_action_url('populate', instance))
|
||||
return self.redirect(self.get_action_url('view', instance))
|
||||
|
||||
def should_populate(self, obj):
|
||||
return True
|
||||
|
@ -1249,8 +1033,8 @@ class MasterView(View):
|
|||
self.Session.flush()
|
||||
return cloned
|
||||
|
||||
def redirect_after_clone(self, instance, mobile=False):
|
||||
return self.redirect(self.get_action_url('view', instance, mobile=mobile))
|
||||
def redirect_after_clone(self, instance, **kwargs):
|
||||
return self.redirect(self.get_action_url('view', instance))
|
||||
|
||||
def touch(self):
|
||||
"""
|
||||
|
@ -1414,75 +1198,6 @@ class MasterView(View):
|
|||
versions.extend(query.all())
|
||||
return versions
|
||||
|
||||
def mobile_view(self):
|
||||
"""
|
||||
Mobile view for displaying a single object's details
|
||||
"""
|
||||
self.mobile = True
|
||||
self.viewing = True
|
||||
instance = self.get_instance()
|
||||
form = self.make_mobile_form(instance)
|
||||
|
||||
context = {
|
||||
'instance': instance,
|
||||
'instance_title': self.get_instance_title(instance),
|
||||
'instance_editable': self.editable_instance(instance),
|
||||
# 'instance_deletable': self.deletable_instance(instance),
|
||||
'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)
|
||||
|
||||
def make_mobile_form(self, instance=None, factory=None, fields=None, schema=None, **kwargs):
|
||||
"""
|
||||
Creates a new mobile form for the given model class/instance.
|
||||
"""
|
||||
if factory is None:
|
||||
factory = self.get_mobile_form_factory()
|
||||
if fields is None:
|
||||
fields = self.get_mobile_form_fields()
|
||||
if schema is None:
|
||||
schema = self.make_mobile_form_schema()
|
||||
|
||||
if not self.creating:
|
||||
kwargs['model_instance'] = instance
|
||||
kwargs = self.make_mobile_form_kwargs(**kwargs)
|
||||
form = factory(fields, schema, **kwargs)
|
||||
self.configure_mobile_form(form)
|
||||
return form
|
||||
|
||||
def get_mobile_form_fields(self):
|
||||
if hasattr(self, 'mobile_form_fields'):
|
||||
return self.mobile_form_fields
|
||||
# TODO
|
||||
# raise NotImplementedError
|
||||
|
||||
def make_mobile_form_schema(self):
|
||||
if not self.model_class:
|
||||
# TODO
|
||||
raise NotImplementedError
|
||||
|
||||
def make_mobile_form_kwargs(self, **kwargs):
|
||||
"""
|
||||
Return a dictionary of kwargs to be passed to the factory when creating
|
||||
new mobile forms.
|
||||
"""
|
||||
defaults = {
|
||||
'request': self.request,
|
||||
'readonly': self.viewing,
|
||||
'model_class': getattr(self, 'model_class', None),
|
||||
'action_url': self.request.current_route_url(_query=None),
|
||||
}
|
||||
if self.creating:
|
||||
defaults['cancel_url'] = self.get_index_url(mobile=True)
|
||||
else:
|
||||
instance = kwargs['model_instance']
|
||||
defaults['cancel_url'] = self.get_action_url('view', instance, mobile=True)
|
||||
defaults.update(kwargs)
|
||||
return defaults
|
||||
|
||||
def configure_common_form(self, form):
|
||||
"""
|
||||
Configure the form in whatever way is deemed "common" - i.e. where
|
||||
|
@ -1491,6 +1206,8 @@ class MasterView(View):
|
|||
By default this removes the 'uuid' field (if present), sets any primary
|
||||
key fields to be readonly (if we have a :attr:`model_class` and are in
|
||||
edit mode), and sets labels as defined by the master class hierarchy.
|
||||
|
||||
TODO: this logic should be moved back into configure_form()
|
||||
"""
|
||||
form.remove_field('uuid')
|
||||
|
||||
|
@ -1516,62 +1233,29 @@ class MasterView(View):
|
|||
# is the safer option and would help prevent unwanted mistakes
|
||||
form.set_default('local_only', True)
|
||||
|
||||
def configure_mobile_form(self, form):
|
||||
"""
|
||||
Configure the main "mobile" form for the view's data model.
|
||||
"""
|
||||
self.configure_common_form(form)
|
||||
|
||||
def validate_mobile_form(self, form):
|
||||
if form.validate(newstyle=True):
|
||||
# TODO: deprecate / remove self.form_deserialized
|
||||
self.form_deserialized = form.validated
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def make_mobile_row_form(self, instance=None, factory=None, fields=None, schema=None, **kwargs):
|
||||
"""
|
||||
Creates a new mobile form for the given model class/instance.
|
||||
"""
|
||||
if factory is None:
|
||||
factory = self.get_mobile_row_form_factory()
|
||||
if fields is None:
|
||||
fields = self.get_mobile_row_form_fields()
|
||||
if schema is None:
|
||||
schema = self.make_mobile_row_form_schema()
|
||||
|
||||
if not self.creating:
|
||||
kwargs['model_instance'] = instance
|
||||
kwargs = self.make_mobile_row_form_kwargs(**kwargs)
|
||||
form = factory(fields, schema, **kwargs)
|
||||
self.configure_mobile_row_form(form)
|
||||
return form
|
||||
|
||||
def make_quick_row_form(self, instance=None, factory=None, fields=None, schema=None, mobile=False, **kwargs):
|
||||
def make_quick_row_form(self, instance=None, factory=None, fields=None, schema=None, **kwargs):
|
||||
"""
|
||||
Creates a "quick" form for adding a new row to the given instance.
|
||||
"""
|
||||
if factory is None:
|
||||
factory = self.get_quick_row_form_factory(mobile=mobile)
|
||||
factory = self.get_quick_row_form_factory()
|
||||
if fields is None:
|
||||
fields = self.get_quick_row_form_fields(mobile=mobile)
|
||||
fields = self.get_quick_row_form_fields()
|
||||
if schema is None:
|
||||
schema = self.make_quick_row_form_schema(mobile=mobile)
|
||||
schema = self.make_quick_row_form_schema()
|
||||
|
||||
kwargs['mobile'] = mobile
|
||||
kwargs = self.make_quick_row_form_kwargs(**kwargs)
|
||||
form = factory(fields, schema, **kwargs)
|
||||
self.configure_quick_row_form(form, mobile=mobile)
|
||||
self.configure_quick_row_form(form)
|
||||
return form
|
||||
|
||||
def get_quick_row_form_factory(self, mobile=False):
|
||||
def get_quick_row_form_factory(self, **kwargs):
|
||||
return forms.Form
|
||||
|
||||
def get_quick_row_form_fields(self, mobile=False):
|
||||
def get_quick_row_form_fields(self, **kwargs):
|
||||
pass
|
||||
|
||||
def make_quick_row_form_schema(self, mobile=False):
|
||||
def make_quick_row_form_schema(self, **kwargs):
|
||||
schema = colander.MappingSchema()
|
||||
schema.add(colander.SchemaNode(colander.String(), name='quick_entry'))
|
||||
return schema
|
||||
|
@ -1585,102 +1269,12 @@ class MasterView(View):
|
|||
defaults.update(kwargs)
|
||||
return defaults
|
||||
|
||||
def configure_quick_row_form(self, form, mobile=False):
|
||||
def configure_quick_row_form(self, form, **kwargs):
|
||||
pass
|
||||
|
||||
def get_mobile_row_form_fields(self):
|
||||
if hasattr(self, 'mobile_row_form_fields'):
|
||||
return self.mobile_row_form_fields
|
||||
# TODO
|
||||
# raise NotImplementedError
|
||||
|
||||
def make_mobile_row_form_schema(self):
|
||||
if not self.model_row_class:
|
||||
# TODO
|
||||
raise NotImplementedError
|
||||
|
||||
def make_mobile_row_form_kwargs(self, **kwargs):
|
||||
"""
|
||||
Return a dictionary of kwargs to be passed to the factory when creating
|
||||
new mobile row forms.
|
||||
"""
|
||||
defaults = {
|
||||
'request': self.request,
|
||||
'mobile': True,
|
||||
'readonly': self.viewing,
|
||||
'model_class': getattr(self, 'model_row_class', None),
|
||||
'action_url': self.request.current_route_url(_query=None),
|
||||
}
|
||||
if self.creating:
|
||||
defaults['cancel_url'] = self.request.get_referrer()
|
||||
else:
|
||||
instance = kwargs['model_instance']
|
||||
defaults['cancel_url'] = self.get_row_action_url('view', instance, mobile=True)
|
||||
defaults.update(kwargs)
|
||||
return defaults
|
||||
|
||||
def configure_mobile_row_form(self, form):
|
||||
"""
|
||||
Configure the mobile row form.
|
||||
"""
|
||||
# TODO: is any of this stuff from configure_form() needed?
|
||||
# if self.editing:
|
||||
# model_class = self.get_model_class(error=False)
|
||||
# if model_class:
|
||||
# mapper = orm.class_mapper(model_class)
|
||||
# for key in mapper.primary_key:
|
||||
# for field in form.fields:
|
||||
# if field == key.name:
|
||||
# form.set_readonly(field)
|
||||
# break
|
||||
# form.remove_field('uuid')
|
||||
|
||||
self.set_row_labels(form)
|
||||
|
||||
def validate_mobile_row_form(self, form):
|
||||
controls = self.request.POST.items()
|
||||
try:
|
||||
self.form_deserialized = form.validate(controls)
|
||||
except deform.ValidationFailure:
|
||||
return False
|
||||
return True
|
||||
|
||||
def validate_quick_row_form(self, form):
|
||||
return form.validate(newstyle=True)
|
||||
|
||||
def get_mobile_row_data(self, parent):
|
||||
query = self.get_row_data(parent)
|
||||
return self.sort_mobile_row_data(query)
|
||||
|
||||
def sort_mobile_row_data(self, query):
|
||||
return query
|
||||
|
||||
def mobile_row_route_url(self, route_name, **kwargs):
|
||||
route_name = 'mobile.{}.{}_row'.format(self.get_route_prefix(), route_name)
|
||||
return self.request.route_url(route_name, **kwargs)
|
||||
|
||||
def mobile_view_row(self):
|
||||
"""
|
||||
Mobile view for row items
|
||||
"""
|
||||
self.mobile = True
|
||||
self.viewing = True
|
||||
row = self.get_row_instance()
|
||||
parent = self.get_parent(row)
|
||||
form = self.make_mobile_row_form(row)
|
||||
context = {
|
||||
'row': row,
|
||||
'parent_instance': parent,
|
||||
'parent_title': self.get_instance_title(parent),
|
||||
'parent_url': self.get_action_url('view', parent, mobile=True),
|
||||
'instance': row,
|
||||
'instance_title': self.get_row_instance_title(row),
|
||||
'instance_editable': self.row_editable(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):
|
||||
if self.rows_creatable:
|
||||
link = tags.link_to("Create a new {}".format(self.get_row_model_title()),
|
||||
|
@ -1851,61 +1445,16 @@ class MasterView(View):
|
|||
context['dform'] = form.make_deform_form()
|
||||
return self.render_to_response('edit', context)
|
||||
|
||||
def mobile_edit(self):
|
||||
"""
|
||||
Mobile view for editing an existing model record.
|
||||
"""
|
||||
self.mobile = True
|
||||
self.editing = True
|
||||
obj = self.get_instance()
|
||||
|
||||
if not self.editable_instance(obj):
|
||||
msg = "Edit is not permitted for {}: {}".format(
|
||||
self.get_model_title(),
|
||||
self.get_instance_title(obj))
|
||||
self.request.session.flash(msg, 'error')
|
||||
return self.redirect(self.get_action_url('view', obj))
|
||||
|
||||
form = self.make_mobile_form(obj)
|
||||
|
||||
if self.request.method == 'POST':
|
||||
if self.validate_mobile_form(form):
|
||||
|
||||
# note that save_form() may return alternate object
|
||||
obj = self.save_mobile_edit_form(form)
|
||||
|
||||
msg = "{} has been updated: {}".format(
|
||||
self.get_model_title(),
|
||||
self.get_instance_title(obj))
|
||||
self.request.session.flash(msg)
|
||||
return self.redirect_after_edit(obj, mobile=True)
|
||||
|
||||
context = {
|
||||
'instance': obj,
|
||||
'instance_title': self.get_instance_title(obj),
|
||||
'instance_deletable': self.deletable_instance(obj),
|
||||
'instance_url': self.get_action_url('view', obj, mobile=True),
|
||||
'form': form,
|
||||
}
|
||||
if hasattr(form, 'make_deform_form'):
|
||||
context['dform'] = form.make_deform_form()
|
||||
return self.render_to_response('edit', context, mobile=True)
|
||||
|
||||
def save_edit_form(self, form):
|
||||
if not self.mobile:
|
||||
uploads = self.normalize_uploads(form)
|
||||
uploads = self.normalize_uploads(form)
|
||||
obj = self.objectify(form)
|
||||
if not self.mobile:
|
||||
self.process_uploads(obj, form, uploads)
|
||||
self.process_uploads(obj, form, uploads)
|
||||
self.after_edit(obj)
|
||||
self.Session.flush()
|
||||
return obj
|
||||
|
||||
def save_mobile_edit_form(self, form):
|
||||
return self.save_edit_form(form)
|
||||
|
||||
def redirect_after_edit(self, instance, mobile=False):
|
||||
return self.redirect(self.get_action_url('view', instance, mobile=mobile))
|
||||
def redirect_after_edit(self, instance, **kwargs):
|
||||
return self.redirect(self.get_action_url('view', instance))
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
|
@ -2350,13 +1899,11 @@ class MasterView(View):
|
|||
"""
|
||||
return getattr(cls, 'permission_prefix', cls.get_route_prefix())
|
||||
|
||||
def get_index_url(self, mobile=False, **kwargs):
|
||||
def get_index_url(self, **kwargs):
|
||||
"""
|
||||
Returns the master view's index URL.
|
||||
"""
|
||||
route = self.get_route_prefix()
|
||||
if mobile:
|
||||
route = 'mobile.{}'.format(route)
|
||||
return self.request.route_url(route, **kwargs)
|
||||
|
||||
@classmethod
|
||||
|
@ -2366,15 +1913,13 @@ class MasterView(View):
|
|||
"""
|
||||
return getattr(cls, 'index_title', cls.get_model_title_plural())
|
||||
|
||||
def get_action_url(self, action, instance, mobile=False, **kwargs):
|
||||
def get_action_url(self, action, instance, **kwargs):
|
||||
"""
|
||||
Generate a URL for the given action on the given instance
|
||||
"""
|
||||
kw = self.get_action_route_kwargs(instance)
|
||||
kw.update(kwargs)
|
||||
route_prefix = self.get_route_prefix()
|
||||
if mobile:
|
||||
route_prefix = 'mobile.{}'.format(route_prefix)
|
||||
return self.request.route_url('{}.{}'.format(route_prefix, action), **kw)
|
||||
|
||||
def get_help_url(self):
|
||||
|
@ -2394,7 +1939,7 @@ class MasterView(View):
|
|||
|
||||
return global_help_url(self.rattail_config)
|
||||
|
||||
def render_to_response(self, template, data, mobile=False):
|
||||
def render_to_response(self, template, data, **kwargs):
|
||||
"""
|
||||
Return a response with the given template rendered with the given data.
|
||||
Note that ``template`` must only be a "key" (e.g. 'index' or 'view').
|
||||
|
@ -2405,13 +1950,12 @@ class MasterView(View):
|
|||
context = {
|
||||
'master': self,
|
||||
'use_buefy': self.get_use_buefy(),
|
||||
'mobile': mobile,
|
||||
'model_title': self.get_model_title(),
|
||||
'model_title_plural': self.get_model_title_plural(),
|
||||
'route_prefix': self.get_route_prefix(),
|
||||
'permission_prefix': self.get_permission_prefix(),
|
||||
'index_title': self.get_index_title(),
|
||||
'index_url': self.get_index_url(mobile=mobile),
|
||||
'index_url': self.get_index_url(),
|
||||
'action_url': self.get_action_url,
|
||||
'grid_index': self.grid_index,
|
||||
'help_url': self.get_help_url(),
|
||||
|
@ -2430,34 +1974,20 @@ class MasterView(View):
|
|||
context['row_model_title_plural'] = self.get_row_model_title_plural()
|
||||
context['row_action_url'] = self.get_row_action_url
|
||||
|
||||
if mobile and self.viewing and self.mobile_rows_quickable:
|
||||
|
||||
# quick row does *not* mimic keyboard wedge by default, but can
|
||||
context['quick_row_keyboard_wedge'] = False
|
||||
|
||||
# quick row does *not* use autocomplete by default, but can
|
||||
context['quick_row_autocomplete'] = False
|
||||
context['quick_row_autocomplete_url'] = '#'
|
||||
|
||||
context.update(data)
|
||||
context.update(self.template_kwargs(**context))
|
||||
if hasattr(self, 'template_kwargs_{}'.format(template)):
|
||||
context.update(getattr(self, 'template_kwargs_{}'.format(template))(**context))
|
||||
if mobile and hasattr(self, 'mobile_template_kwargs_{}'.format(template)):
|
||||
context.update(getattr(self, 'mobile_template_kwargs_{}'.format(template))(**context))
|
||||
|
||||
# First try the template path most specific to the view.
|
||||
if mobile:
|
||||
mako_path = '/mobile{}/{}.mako'.format(self.get_template_prefix(), template)
|
||||
else:
|
||||
mako_path = '{}/{}.mako'.format(self.get_template_prefix(), template)
|
||||
mako_path = '{}/{}.mako'.format(self.get_template_prefix(), template)
|
||||
try:
|
||||
return render_to_response(mako_path, context, request=self.request)
|
||||
|
||||
except IOError:
|
||||
|
||||
# Failing that, try one or more fallback templates.
|
||||
for fallback in self.get_fallback_templates(template, mobile=mobile):
|
||||
for fallback in self.get_fallback_templates(template):
|
||||
try:
|
||||
return render_to_response(fallback, context, request=self.request)
|
||||
except IOError:
|
||||
|
@ -2504,9 +2034,7 @@ class MasterView(View):
|
|||
return render('{}/{}.mako'.format(self.get_template_prefix(), template),
|
||||
context, request=self.request)
|
||||
|
||||
def get_fallback_templates(self, template, mobile=False):
|
||||
if mobile:
|
||||
return ['/mobile/master/{}.mako'.format(template)]
|
||||
def get_fallback_templates(self, template, **kwargs):
|
||||
return ['/master/{}.mako'.format(template)]
|
||||
|
||||
def get_default_engine_dbkey(self):
|
||||
|
@ -3736,14 +3264,6 @@ class MasterView(View):
|
|||
"""
|
||||
return getattr(cls, 'form_factory', forms.Form)
|
||||
|
||||
@classmethod
|
||||
def get_mobile_form_factory(cls):
|
||||
"""
|
||||
Returns the factory or class which is to be used when creating new
|
||||
mobile forms.
|
||||
"""
|
||||
return getattr(cls, 'mobile_form_factory', forms.Form)
|
||||
|
||||
@classmethod
|
||||
def get_row_form_factory(cls):
|
||||
"""
|
||||
|
@ -3752,14 +3272,6 @@ class MasterView(View):
|
|||
"""
|
||||
return getattr(cls, 'row_form_factory', forms.Form)
|
||||
|
||||
@classmethod
|
||||
def get_mobile_row_form_factory(cls):
|
||||
"""
|
||||
Returns the factory or class which is to be used when creating new
|
||||
mobile row forms.
|
||||
"""
|
||||
return getattr(cls, 'mobile_row_form_factory', forms.Form)
|
||||
|
||||
def download_path(self, obj, filename):
|
||||
"""
|
||||
Should return absolute path on disk, for the given object and filename.
|
||||
|
@ -4055,49 +3567,8 @@ class MasterView(View):
|
|||
def after_create_row(self, row_object):
|
||||
pass
|
||||
|
||||
def redirect_after_create_row(self, row, mobile=False):
|
||||
return self.redirect(self.get_row_action_url('view', row, mobile=mobile))
|
||||
|
||||
def mobile_create_row(self):
|
||||
"""
|
||||
Mobile view for creating a new row object
|
||||
"""
|
||||
self.mobile = True
|
||||
self.creating = True
|
||||
parent = self.get_instance()
|
||||
instance_url = self.get_action_url('view', parent, mobile=True)
|
||||
form = self.make_mobile_row_form(self.model_row_class, cancel_url=instance_url)
|
||||
if self.request.method == 'POST':
|
||||
if self.validate_mobile_row_form(form):
|
||||
self.before_create_row(form)
|
||||
# let save() return alternate object if necessary
|
||||
obj = self.save_create_row_form(form)
|
||||
self.after_create_row(obj)
|
||||
return self.redirect_after_create_row(obj, mobile=True)
|
||||
return self.render_to_response('create_row', {
|
||||
'instance_title': self.get_instance_title(parent),
|
||||
'instance_url': instance_url,
|
||||
'parent_object': parent,
|
||||
'form': form,
|
||||
}, mobile=True)
|
||||
|
||||
def mobile_quick_row(self):
|
||||
"""
|
||||
Mobile view for "quick" location or creation of a row object
|
||||
"""
|
||||
parent = self.get_instance()
|
||||
parent_url = self.get_action_url('view', parent, mobile=True)
|
||||
form = self.make_quick_row_form(self.model_row_class, mobile=True, cancel_url=parent_url)
|
||||
if self.request.method == 'POST':
|
||||
if self.validate_quick_row_form(form):
|
||||
row = self.save_quick_row_form(form)
|
||||
if not row:
|
||||
self.request.session.flash("Could not locate/create row for entry: "
|
||||
"{}".format(form.validated['quick_entry']),
|
||||
'error')
|
||||
return self.redirect(parent_url)
|
||||
return self.redirect_after_quick_row(row, mobile=True)
|
||||
return self.redirect(parent_url)
|
||||
def redirect_after_create_row(self, row, **kwargs):
|
||||
return self.redirect(self.get_row_action_url('view', row))
|
||||
|
||||
def save_quick_row_form(self, form):
|
||||
raise NotImplementedError("You must define `{}:{}.save_quick_row_form()` "
|
||||
|
@ -4105,8 +3576,8 @@ class MasterView(View):
|
|||
self.__class__.__module__,
|
||||
self.__class__.__name__))
|
||||
|
||||
def redirect_after_quick_row(self, row, mobile=False):
|
||||
return self.redirect(self.get_row_action_url('edit', row, mobile=mobile))
|
||||
def redirect_after_quick_row(self, row, **kwargs):
|
||||
return self.redirect(self.get_row_action_url('edit', row))
|
||||
|
||||
def view_row(self):
|
||||
"""
|
||||
|
@ -4182,34 +3653,6 @@ class MasterView(View):
|
|||
'dform': form.make_deform_form(),
|
||||
})
|
||||
|
||||
def mobile_edit_row(self):
|
||||
"""
|
||||
Mobile view for editing a row object
|
||||
"""
|
||||
self.mobile = True
|
||||
self.editing = True
|
||||
row = self.get_row_instance()
|
||||
instance_url = self.get_row_action_url('view', row, mobile=True)
|
||||
form = self.make_mobile_row_form(row)
|
||||
|
||||
if self.request.method == 'POST':
|
||||
if self.validate_mobile_row_form(form):
|
||||
self.save_edit_row_form(form)
|
||||
return self.redirect_after_edit_row(row, mobile=True)
|
||||
|
||||
parent = self.get_parent(row)
|
||||
return self.render_to_response('edit_row', {
|
||||
'row': row,
|
||||
'instance': row,
|
||||
'parent_instance': parent,
|
||||
'instance_title': self.get_row_instance_title(row),
|
||||
'instance_url': instance_url,
|
||||
'instance_deletable': self.row_deletable(row),
|
||||
'parent_title': self.get_instance_title(parent),
|
||||
'parent_url': self.get_action_url('view', parent, mobile=True),
|
||||
'form': form},
|
||||
mobile=True)
|
||||
|
||||
def save_edit_row_form(self, form):
|
||||
obj = self.objectify(form, self.form_deserialized)
|
||||
self.after_edit_row(obj)
|
||||
|
@ -4224,8 +3667,8 @@ class MasterView(View):
|
|||
Event hook, called just after an existing row object is saved.
|
||||
"""
|
||||
|
||||
def redirect_after_edit_row(self, row, mobile=False):
|
||||
return self.redirect(self.get_row_action_url('view', row, mobile=mobile))
|
||||
def redirect_after_edit_row(self, row, **kwargs):
|
||||
return self.redirect(self.get_row_action_url('view', row))
|
||||
|
||||
def row_deletable(self, row):
|
||||
"""
|
||||
|
@ -4252,22 +3695,6 @@ class MasterView(View):
|
|||
self.delete_row_object(row)
|
||||
return self.redirect(self.get_action_url('view', self.get_parent(row)))
|
||||
|
||||
def mobile_delete_row(self):
|
||||
"""
|
||||
Mobile view which can "delete" a sub-row from the parent.
|
||||
"""
|
||||
if self.request.method == 'POST':
|
||||
parent = self.get_instance()
|
||||
row = self.get_row_instance()
|
||||
if self.get_parent(row) is not parent:
|
||||
raise RuntimeError("Can only delete rows which belong to current object")
|
||||
|
||||
self.delete_row_object(row)
|
||||
return self.redirect(self.get_action_url('view', parent, mobile=True))
|
||||
|
||||
self.session.flash("Must POST to delete a row", 'error')
|
||||
return self.redirect(self.request.get_referrer(mobile=True))
|
||||
|
||||
def get_parent(self, row):
|
||||
raise NotImplementedError
|
||||
|
||||
|
@ -4357,13 +3784,11 @@ class MasterView(View):
|
|||
return True
|
||||
return False
|
||||
|
||||
def get_row_action_url(self, action, row, mobile=False):
|
||||
def get_row_action_url(self, action, row, **kwargs):
|
||||
"""
|
||||
Generate a URL for the given action on the given row.
|
||||
"""
|
||||
route_name = '{}.{}_row'.format(self.get_route_prefix(), action)
|
||||
if mobile:
|
||||
route_name = 'mobile.{}'.format(route_name)
|
||||
return self.request.route_url(route_name, **self.get_row_action_route_kwargs(row))
|
||||
|
||||
def get_row_action_route_kwargs(self, row):
|
||||
|
@ -4426,7 +3851,6 @@ class MasterView(View):
|
|||
model_title_plural = cls.get_model_title_plural()
|
||||
if cls.has_rows:
|
||||
row_model_title = cls.get_row_model_title()
|
||||
legacy_mobile = cls.legacy_mobile_enabled(rattail_config)
|
||||
|
||||
config.add_tailbone_permission_group(permission_prefix, model_title_plural, overwrite=False)
|
||||
|
||||
|
@ -4437,10 +3861,6 @@ class MasterView(View):
|
|||
config.add_route(route_prefix, '{}/'.format(url_prefix))
|
||||
config.add_view(cls, attr='index', route_name=route_prefix,
|
||||
permission='{}.list'.format(permission_prefix))
|
||||
if legacy_mobile and cls.supports_mobile:
|
||||
config.add_route('mobile.{}'.format(route_prefix), '/mobile{}/'.format(url_prefix))
|
||||
config.add_view(cls, attr='mobile_index', route_name='mobile.{}'.format(route_prefix),
|
||||
permission='{}.list'.format(permission_prefix))
|
||||
|
||||
# download results
|
||||
# this is the "new" more flexible approach, but we only want to
|
||||
|
@ -4495,17 +3915,12 @@ class MasterView(View):
|
|||
permission='{}.quickie'.format(permission_prefix))
|
||||
|
||||
# create
|
||||
if cls.creatable or (legacy_mobile and cls.mobile_creatable):
|
||||
if cls.creatable:
|
||||
config.add_tailbone_permission(permission_prefix, '{}.create'.format(permission_prefix),
|
||||
"Create new {}".format(model_title))
|
||||
if cls.creatable:
|
||||
config.add_route('{}.create'.format(route_prefix), '{}/new'.format(url_prefix))
|
||||
config.add_view(cls, attr='create', route_name='{}.create'.format(route_prefix),
|
||||
permission='{}.create'.format(permission_prefix))
|
||||
if legacy_mobile and cls.mobile_creatable:
|
||||
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))
|
||||
|
||||
# populate new object
|
||||
if cls.populatable:
|
||||
|
@ -4572,10 +3987,6 @@ class MasterView(View):
|
|||
config.add_route('{}.view'.format(route_prefix), instance_url_prefix)
|
||||
config.add_view(cls, attr='view', route_name='{}.view'.format(route_prefix),
|
||||
permission='{}.view'.format(permission_prefix))
|
||||
if legacy_mobile and cls.supports_mobile:
|
||||
config.add_route('mobile.{}.view'.format(route_prefix), '/mobile{}'.format(instance_url_prefix))
|
||||
config.add_view(cls, attr='mobile_view', route_name='mobile.{}.view'.format(route_prefix),
|
||||
permission='{}.view'.format(permission_prefix))
|
||||
|
||||
# version history
|
||||
if cls.has_versions and rattail_config and rattail_config.versioning_enabled():
|
||||
|
@ -4625,30 +4036,20 @@ class MasterView(View):
|
|||
"Download associated data for {}".format(model_title))
|
||||
|
||||
# edit
|
||||
if cls.editable or (legacy_mobile and cls.mobile_editable):
|
||||
if cls.editable:
|
||||
config.add_tailbone_permission(permission_prefix, '{}.edit'.format(permission_prefix),
|
||||
"Edit {}".format(model_title))
|
||||
if cls.editable:
|
||||
config.add_route('{}.edit'.format(route_prefix), '{}/edit'.format(instance_url_prefix))
|
||||
config.add_view(cls, attr='edit', route_name='{}.edit'.format(route_prefix),
|
||||
permission='{}.edit'.format(permission_prefix))
|
||||
if legacy_mobile and cls.mobile_editable:
|
||||
config.add_route('mobile.{}.edit'.format(route_prefix), '/mobile{}/edit'.format(instance_url_prefix))
|
||||
config.add_view(cls, attr='mobile_edit', route_name='mobile.{}.edit'.format(route_prefix),
|
||||
permission='{}.edit'.format(permission_prefix))
|
||||
|
||||
# execute
|
||||
if cls.executable or (legacy_mobile and cls.mobile_executable):
|
||||
if cls.executable:
|
||||
config.add_tailbone_permission(permission_prefix, '{}.execute'.format(permission_prefix),
|
||||
"Execute {}".format(model_title))
|
||||
if cls.executable:
|
||||
config.add_route('{}.execute'.format(route_prefix), '{}/execute'.format(instance_url_prefix))
|
||||
config.add_view(cls, attr='execute', route_name='{}.execute'.format(route_prefix),
|
||||
permission='{}.execute'.format(permission_prefix))
|
||||
if legacy_mobile and cls.mobile_executable:
|
||||
config.add_route('mobile.{}.execute'.format(route_prefix), '/mobile{}/execute'.format(instance_url_prefix))
|
||||
config.add_view(cls, attr='mobile_execute', route_name='mobile.{}.execute'.format(route_prefix),
|
||||
permission='{}.execute'.format(permission_prefix))
|
||||
|
||||
# delete
|
||||
if cls.deletable:
|
||||
|
@ -4683,21 +4084,12 @@ class MasterView(View):
|
|||
|
||||
# create row
|
||||
if cls.has_rows:
|
||||
if cls.rows_creatable or (legacy_mobile and cls.mobile_rows_creatable):
|
||||
if cls.rows_creatable:
|
||||
config.add_tailbone_permission(permission_prefix, '{}.create_row'.format(permission_prefix),
|
||||
"Create new {} rows".format(model_title))
|
||||
if cls.rows_creatable:
|
||||
config.add_route('{}.create_row'.format(route_prefix), '{}/new-row'.format(instance_url_prefix))
|
||||
config.add_view(cls, attr='create_row', route_name='{}.create_row'.format(route_prefix),
|
||||
permission='{}.create_row'.format(permission_prefix))
|
||||
if legacy_mobile and cls.mobile_rows_creatable:
|
||||
config.add_route('mobile.{}.create_row'.format(route_prefix), '/mobile{}/new-row'.format(instance_url_prefix))
|
||||
config.add_view(cls, attr='mobile_create_row', route_name='mobile.{}.create_row'.format(route_prefix),
|
||||
permission='{}.create_row'.format(permission_prefix))
|
||||
if cls.mobile_rows_quickable:
|
||||
config.add_route('mobile.{}.quick_row'.format(route_prefix), '/mobile{}/quick-row'.format(instance_url_prefix))
|
||||
config.add_view(cls, attr='mobile_quick_row', route_name='mobile.{}.quick_row'.format(route_prefix),
|
||||
permission='{}.create_row'.format(permission_prefix))
|
||||
|
||||
# view row
|
||||
if cls.has_rows:
|
||||
|
@ -4705,35 +4097,21 @@ class MasterView(View):
|
|||
config.add_route('{}.view_row'.format(route_prefix), '{}/{{uuid}}/rows/{{row_uuid}}'.format(url_prefix))
|
||||
config.add_view(cls, attr='view_row', route_name='{}.view_row'.format(route_prefix),
|
||||
permission='{}.view'.format(permission_prefix))
|
||||
if legacy_mobile and cls.mobile_rows_viewable:
|
||||
config.add_route('mobile.{}.view_row'.format(route_prefix), '/mobile{}/{{uuid}}/rows/{{row_uuid}}'.format(url_prefix))
|
||||
config.add_view(cls, attr='mobile_view_row', route_name='mobile.{}.view_row'.format(route_prefix),
|
||||
permission='{}.view'.format(permission_prefix))
|
||||
|
||||
# edit row
|
||||
if cls.has_rows:
|
||||
if cls.rows_editable or (legacy_mobile and cls.mobile_rows_editable):
|
||||
if cls.rows_editable:
|
||||
config.add_tailbone_permission(permission_prefix, '{}.edit_row'.format(permission_prefix),
|
||||
"Edit individual {} rows".format(model_title))
|
||||
if cls.rows_editable:
|
||||
config.add_route('{}.edit_row'.format(route_prefix), '{}/{{uuid}}/rows/{{row_uuid}}/edit'.format(url_prefix))
|
||||
config.add_view(cls, attr='edit_row', route_name='{}.edit_row'.format(route_prefix),
|
||||
permission='{}.edit_row'.format(permission_prefix))
|
||||
if legacy_mobile and cls.mobile_rows_editable:
|
||||
config.add_route('mobile.{}.edit_row'.format(route_prefix), '/mobile{}/{{uuid}}/rows/{{row_uuid}}/edit'.format(url_prefix))
|
||||
config.add_view(cls, attr='mobile_edit_row', route_name='mobile.{}.edit_row'.format(route_prefix),
|
||||
permission='{}.edit_row'.format(permission_prefix))
|
||||
|
||||
# delete row
|
||||
if cls.has_rows:
|
||||
if cls.rows_deletable or (legacy_mobile and cls.mobile_rows_deletable):
|
||||
if cls.rows_deletable:
|
||||
config.add_tailbone_permission(permission_prefix, '{}.delete_row'.format(permission_prefix),
|
||||
"Delete individual {} rows".format(model_title))
|
||||
if cls.rows_deletable:
|
||||
config.add_route('{}.delete_row'.format(route_prefix), '{}/{{uuid}}/rows/{{row_uuid}}/delete'.format(url_prefix))
|
||||
config.add_view(cls, attr='delete_row', route_name='{}.delete_row'.format(route_prefix),
|
||||
permission='{}.delete_row'.format(permission_prefix))
|
||||
if legacy_mobile and cls.mobile_rows_deletable:
|
||||
config.add_route('mobile.{}.delete_row'.format(route_prefix), '/mobile{}/{{uuid}}/rows/{{row_uuid}}/delete'.format(url_prefix))
|
||||
config.add_view(cls, attr='mobile_delete_row', route_name='mobile.{}.delete_row'.format(route_prefix),
|
||||
permission='{}.delete_row'.format(permission_prefix))
|
||||
|
|
|
@ -53,7 +53,6 @@ class PersonView(MasterView):
|
|||
route_prefix = 'people'
|
||||
touchable = True
|
||||
has_versions = True
|
||||
supports_mobile = True
|
||||
bulk_deletable = True
|
||||
is_contact = True
|
||||
manage_notes_from_profile_view = False
|
||||
|
@ -85,19 +84,6 @@ class PersonView(MasterView):
|
|||
'users',
|
||||
]
|
||||
|
||||
mobile_form_fields = [
|
||||
'first_name',
|
||||
'middle_name',
|
||||
'last_name',
|
||||
'display_name',
|
||||
'phone',
|
||||
'email',
|
||||
'address',
|
||||
'employee',
|
||||
'customers',
|
||||
'users',
|
||||
]
|
||||
|
||||
mergeable = True
|
||||
merge_additive_fields = [
|
||||
'usernames',
|
||||
|
@ -331,8 +317,7 @@ class PersonView(MasterView):
|
|||
text = "(#{}) {}".format(customer.number, text)
|
||||
elif customer.id:
|
||||
text = "({}) {}".format(customer.id, text)
|
||||
route = '{}customers.view'.format('mobile.' if self.mobile else '')
|
||||
url = self.request.route_url(route, uuid=customer.uuid)
|
||||
url = self.request.route_url('customers.view', uuid=customer.uuid)
|
||||
items.append(HTML.tag('li', c=[tags.link_to(text, url)]))
|
||||
return HTML.tag('ul', c=items)
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2019 Lance Edgar
|
||||
# Copyright © 2010-2021 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
|
@ -44,10 +44,10 @@ class PrincipalMasterView(MasterView):
|
|||
Master view base class for security principal models, i.e. User and Role.
|
||||
"""
|
||||
|
||||
def get_fallback_templates(self, template, mobile=False):
|
||||
def get_fallback_templates(self, template, **kwargs):
|
||||
return [
|
||||
'/principal/{}.mako'.format(template),
|
||||
] + super(PrincipalMasterView, self).get_fallback_templates(template, mobile=mobile)
|
||||
] + super(PrincipalMasterView, self).get_fallback_templates(template, **kwargs)
|
||||
|
||||
def perm_sortkey(self, item):
|
||||
key, value = item
|
||||
|
|
|
@ -82,7 +82,6 @@ class ProductView(MasterView):
|
|||
Master view for the Product class.
|
||||
"""
|
||||
model_class = model.Product
|
||||
supports_mobile = True
|
||||
has_versions = True
|
||||
results_downloadable_xlsx = True
|
||||
|
||||
|
@ -157,8 +156,6 @@ class ProductView(MasterView):
|
|||
'inventory_on_order',
|
||||
]
|
||||
|
||||
mobile_form_fields = form_fields
|
||||
|
||||
# These aliases enable the grid queries to filter products which may be
|
||||
# purchased from *any* vendor, and yet sort by only the "preferred" vendor
|
||||
# (since that's what shows up in the grid column).
|
||||
|
@ -936,7 +933,7 @@ class ProductView(MasterView):
|
|||
else:
|
||||
code = pack.item_id
|
||||
text = "({}) {}".format(code, pack.full_description)
|
||||
url = self.get_action_url('view', pack, mobile=self.mobile)
|
||||
url = self.get_action_url('view', pack)
|
||||
links.append(tags.link_to(text, url))
|
||||
|
||||
items = [HTML.tag('li', c=[link]) for link in links]
|
||||
|
@ -955,7 +952,7 @@ class ProductView(MasterView):
|
|||
code = unit.item_id
|
||||
|
||||
text = "({}) {}".format(code, unit.full_description)
|
||||
url = self.get_action_url('view', unit, mobile=self.mobile)
|
||||
url = self.get_action_url('view', unit)
|
||||
return tags.link_to(text, url)
|
||||
|
||||
def render_current_price_ends(self, product, field):
|
||||
|
@ -1494,37 +1491,6 @@ class ProductView(MasterView):
|
|||
'instance_title': self.get_instance_title(instance),
|
||||
'form': form})
|
||||
|
||||
def mobile_index(self):
|
||||
"""
|
||||
Mobile "home" page for products
|
||||
"""
|
||||
self.mobile = True
|
||||
context = {
|
||||
'quick_lookup': False,
|
||||
'placeholder': "Enter {}".format(self.rattail_config.product_key_title()),
|
||||
'quick_lookup_keyboard_wedge': True,
|
||||
}
|
||||
if self.rattail_config.getbool('rattail', 'products.mobile.quick_lookup', default=False):
|
||||
context['quick_lookup'] = True
|
||||
else:
|
||||
self.listing = True
|
||||
grid = self.make_mobile_grid()
|
||||
context['grid'] = grid
|
||||
return self.render_to_response('index', context, mobile=True)
|
||||
|
||||
def mobile_quick_lookup(self):
|
||||
entry = self.request.POST['quick_entry'].strip()
|
||||
provided = GPC(entry, calc_check_digit=False)
|
||||
product = api.get_product_by_upc(self.Session(), provided)
|
||||
if not product:
|
||||
checked = GPC(entry, calc_check_digit='upc')
|
||||
product = api.get_product_by_upc(self.Session(), checked)
|
||||
if not product:
|
||||
product = api.get_product_by_code(self.Session(), entry)
|
||||
if not product:
|
||||
raise self.notfound()
|
||||
return self.redirect(self.get_action_url('view', product, mobile=True))
|
||||
|
||||
def get_version_child_classes(self):
|
||||
return [
|
||||
(model.ProductCode, 'product_uuid'),
|
||||
|
@ -1746,7 +1712,6 @@ class ProductView(MasterView):
|
|||
template_prefix = cls.get_template_prefix()
|
||||
permission_prefix = cls.get_permission_prefix()
|
||||
model_title = cls.get_model_title()
|
||||
legacy_mobile = cls.legacy_mobile_enabled(rattail_config)
|
||||
|
||||
# print labels
|
||||
config.add_tailbone_permission('products', 'products.print_labels',
|
||||
|
@ -1787,11 +1752,6 @@ class ProductView(MasterView):
|
|||
renderer='json',
|
||||
permission='{}.versions'.format(permission_prefix))
|
||||
|
||||
# mobile quick lookup
|
||||
if legacy_mobile:
|
||||
config.add_route('mobile.products.quick_lookup', '/mobile/products/quick-lookup')
|
||||
config.add_view(cls, attr='mobile_quick_lookup', route_name='mobile.products.quick_lookup')
|
||||
|
||||
# TODO: deprecate / remove this
|
||||
ProductsView = ProductView
|
||||
|
||||
|
|
|
@ -150,34 +150,6 @@ class PurchasingBatchView(BatchMasterView):
|
|||
'credits',
|
||||
]
|
||||
|
||||
mobile_row_form_fields = [
|
||||
'upc',
|
||||
'item_id',
|
||||
'product',
|
||||
'brand_name',
|
||||
'description',
|
||||
'size',
|
||||
'case_quantity',
|
||||
'cases_ordered',
|
||||
'units_ordered',
|
||||
'cases_received',
|
||||
'units_received',
|
||||
'cases_damaged',
|
||||
'units_damaged',
|
||||
'cases_expired',
|
||||
'units_expired',
|
||||
'cases_mispick',
|
||||
'units_mispick',
|
||||
# 'po_line_number',
|
||||
'po_unit_cost',
|
||||
'po_total',
|
||||
# 'invoice_line_number',
|
||||
'invoice_unit_cost',
|
||||
'invoice_total',
|
||||
'status_code',
|
||||
# 'credits',
|
||||
]
|
||||
|
||||
@property
|
||||
def batch_mode(self):
|
||||
raise NotImplementedError("Please define `batch_mode` for your purchasing batch view")
|
||||
|
@ -518,8 +490,8 @@ class PurchasingBatchView(BatchMasterView):
|
|||
total = purchase.invoice_total
|
||||
return '{} for ${:0,.2f} ({})'.format(date, total or 0, purchase.department or purchase.buyer)
|
||||
|
||||
def get_batch_kwargs(self, batch, mobile=False):
|
||||
kwargs = super(PurchasingBatchView, self).get_batch_kwargs(batch, mobile=mobile)
|
||||
def get_batch_kwargs(self, batch, **kwargs):
|
||||
kwargs = super(PurchasingBatchView, self).get_batch_kwargs(batch, **kwargs)
|
||||
kwargs['mode'] = self.batch_mode
|
||||
kwargs['truck_dump'] = batch.truck_dump
|
||||
kwargs['invoice_parser_key'] = batch.invoice_parser_key
|
||||
|
@ -596,9 +568,6 @@ class PurchasingBatchView(BatchMasterView):
|
|||
# query = super(PurchasingBatchView, self).get_row_data(batch)
|
||||
# return query.options(orm.joinedload(model.PurchaseBatchRow.credits))
|
||||
|
||||
def sort_mobile_row_data(self, query):
|
||||
return query.order_by(model.PurchaseBatchRow.modified.desc())
|
||||
|
||||
def configure_row_grid(self, g):
|
||||
super(PurchasingBatchView, self).configure_row_grid(g)
|
||||
|
||||
|
@ -760,104 +729,6 @@ class PurchasingBatchView(BatchMasterView):
|
|||
g.set_type('credit_total', 'currency')
|
||||
return HTML.literal(g.render_grid())
|
||||
|
||||
def configure_mobile_row_form(self, f):
|
||||
super(PurchasingBatchView, self).configure_mobile_row_form(f)
|
||||
# row = f.model_instance
|
||||
# if self.creating:
|
||||
# batch = self.get_instance()
|
||||
# else:
|
||||
# batch = self.get_parent(row)
|
||||
|
||||
# # readonly fields
|
||||
# f.set_readonly('case_quantity')
|
||||
# f.set_readonly('credits')
|
||||
|
||||
# quantity fields
|
||||
f.set_type('case_quantity', 'quantity')
|
||||
f.set_type('cases_ordered', 'quantity')
|
||||
f.set_type('units_ordered', 'quantity')
|
||||
f.set_type('cases_received', 'quantity')
|
||||
f.set_type('units_received', 'quantity')
|
||||
f.set_type('cases_damaged', 'quantity')
|
||||
f.set_type('units_damaged', 'quantity')
|
||||
f.set_type('cases_expired', 'quantity')
|
||||
f.set_type('units_expired', 'quantity')
|
||||
f.set_type('cases_mispick', 'quantity')
|
||||
f.set_type('units_mispick', 'quantity')
|
||||
|
||||
# currency fields
|
||||
f.set_type('po_unit_cost', 'currency')
|
||||
f.set_type('po_total', 'currency')
|
||||
f.set_type('po_total_calculated', 'currency')
|
||||
f.set_type('invoice_unit_cost', 'currency')
|
||||
f.set_type('invoice_total', 'currency')
|
||||
f.set_type('invoice_total_calculated', 'currency')
|
||||
|
||||
# if self.creating:
|
||||
# f.remove_fields(
|
||||
# 'upc',
|
||||
# 'product',
|
||||
# 'po_total',
|
||||
# 'invoice_total',
|
||||
# )
|
||||
# if self.batch_mode == self.enum.PURCHASE_BATCH_MODE_ORDERING:
|
||||
# f.remove_fields('cases_received',
|
||||
# 'units_received')
|
||||
# elif self.batch_mode == self.enum.PURCHASE_BATCH_MODE_RECEIVING:
|
||||
# f.remove_fields('cases_ordered',
|
||||
# 'units_ordered')
|
||||
|
||||
# elif self.editing:
|
||||
# f.set_readonly('upc')
|
||||
# f.set_readonly('product')
|
||||
# f.remove_fields('po_total',
|
||||
# 'invoice_total',
|
||||
# 'status_code')
|
||||
|
||||
# elif self.viewing:
|
||||
# if row.product:
|
||||
# f.remove_fields('brand_name',
|
||||
# 'description',
|
||||
# 'size')
|
||||
# else:
|
||||
# f.remove_field('product')
|
||||
|
||||
def mobile_new_product(self):
|
||||
"""
|
||||
View which allows user to create a new Product and add a row for it to
|
||||
the Purchasing Batch.
|
||||
"""
|
||||
batch = self.get_instance()
|
||||
batch_url = self.get_action_url('view', batch, mobile=True)
|
||||
form = forms.Form(schema=self.make_new_product_schema(),
|
||||
request=self.request,
|
||||
mobile=True,
|
||||
cancel_url=batch_url)
|
||||
|
||||
if form.validate(newstyle=True):
|
||||
product = model.Product()
|
||||
product.item_id = form.validated['item_id']
|
||||
product.description = form.validated['description']
|
||||
row = self.model_row_class()
|
||||
row.product = product
|
||||
self.handler.add_row(batch, row)
|
||||
self.Session.flush()
|
||||
return self.redirect(self.get_row_action_url('edit', row, mobile=True))
|
||||
|
||||
return self.render_to_response('new_product', {
|
||||
'form': form,
|
||||
'dform': form.make_deform_form(),
|
||||
'instance_title': self.get_instance_title(batch),
|
||||
'instance_url': batch_url,
|
||||
}, mobile=True)
|
||||
|
||||
def make_new_product_schema(self):
|
||||
"""
|
||||
Must return a ``colander.Schema`` instance for use with the form in the
|
||||
:meth:`mobile_new_product()` view.
|
||||
"""
|
||||
return NewProduct()
|
||||
|
||||
# def item_lookup(self, value, field=None):
|
||||
# """
|
||||
# Try to locate a single product using ``value`` as a lookup code.
|
||||
|
@ -956,9 +827,9 @@ class PurchasingBatchView(BatchMasterView):
|
|||
# return self.redirect(self.request.current_route_url())
|
||||
|
||||
# TODO: seems like this should be master behavior, controlled by setting?
|
||||
def redirect_after_edit_row(self, row, mobile=False):
|
||||
def redirect_after_edit_row(self, row, **kwargs):
|
||||
parent = self.get_parent(row)
|
||||
return self.redirect(self.get_action_url('view', parent, mobile=mobile))
|
||||
return self.redirect(self.get_action_url('view', parent))
|
||||
|
||||
# def get_execute_success_url(self, batch, result, **kwargs):
|
||||
# # if batch execution yielded a Purchase, redirect to it
|
||||
|
@ -977,21 +848,12 @@ class PurchasingBatchView(BatchMasterView):
|
|||
permission_prefix = cls.get_permission_prefix()
|
||||
model_key = cls.get_model_key()
|
||||
model_title = cls.get_model_title()
|
||||
legacy_mobile = cls.legacy_mobile_enabled(rattail_config)
|
||||
|
||||
# eligible purchases (AJAX)
|
||||
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),
|
||||
renderer='json', permission='{}.view'.format(permission_prefix))
|
||||
|
||||
# add new product
|
||||
if legacy_mobile and cls.supports_new_product:
|
||||
config.add_tailbone_permission(permission_prefix, '{}.new_product'.format(permission_prefix),
|
||||
"Create new Product when adding row to {}".format(model_title))
|
||||
config.add_route('mobile.{}.new_product'.format(route_prefix), '{}/{{{}}}/new-product'.format(url_prefix, model_key))
|
||||
config.add_view(cls, attr='mobile_new_product', route_name='mobile.{}.new_product'.format(route_prefix),
|
||||
permission='{}.new_product'.format(permission_prefix))
|
||||
|
||||
|
||||
@classmethod
|
||||
def defaults(cls, config):
|
||||
|
|
|
@ -51,12 +51,7 @@ class OrderingBatchView(PurchasingBatchView):
|
|||
model_title = "Ordering Batch"
|
||||
model_title_plural = "Ordering Batches"
|
||||
index_title = "Ordering"
|
||||
mobile_creatable = True
|
||||
rows_editable = True
|
||||
mobile_rows_creatable = True
|
||||
mobile_rows_quickable = True
|
||||
mobile_rows_editable = True
|
||||
mobile_rows_deletable = True
|
||||
has_worksheet = True
|
||||
|
||||
labels = {
|
||||
|
@ -86,21 +81,6 @@ class OrderingBatchView(PurchasingBatchView):
|
|||
'executed_by',
|
||||
]
|
||||
|
||||
mobile_form_fields = [
|
||||
'vendor',
|
||||
'department',
|
||||
'date_ordered',
|
||||
'po_number',
|
||||
'po_total',
|
||||
'created',
|
||||
'created_by',
|
||||
'notes',
|
||||
'status_code',
|
||||
'complete',
|
||||
'executed',
|
||||
'executed_by',
|
||||
]
|
||||
|
||||
row_labels = {
|
||||
'po_total_calculated': "PO Total",
|
||||
}
|
||||
|
@ -161,8 +141,8 @@ class OrderingBatchView(PurchasingBatchView):
|
|||
if self.creating or not batch.executed or not batch.purchase:
|
||||
f.remove_field('purchase')
|
||||
|
||||
def get_batch_kwargs(self, batch, mobile=False):
|
||||
kwargs = super(OrderingBatchView, self).get_batch_kwargs(batch, mobile=mobile)
|
||||
def get_batch_kwargs(self, batch, **kwargs):
|
||||
kwargs = super(OrderingBatchView, self).get_batch_kwargs(batch, **kwargs)
|
||||
kwargs['ship_method'] = batch.ship_method
|
||||
kwargs['notes_to_vendor'] = batch.notes_to_vendor
|
||||
return kwargs
|
||||
|
@ -387,60 +367,6 @@ class OrderingBatchView(PurchasingBatchView):
|
|||
'batch_po_total_display': '${:0,.2f}'.format(batch.po_total_calculated or batch.po_total or 0),
|
||||
}
|
||||
|
||||
def render_mobile_listitem(self, batch, i):
|
||||
return "({}) {} on {} for ${:0,.2f}".format(batch.id_str, batch.vendor,
|
||||
batch.date_ordered, batch.po_total or 0)
|
||||
|
||||
def mobile_create(self):
|
||||
"""
|
||||
Mobile view for creating a new ordering 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:
|
||||
|
||||
# fetch first to avoid flush below
|
||||
store = self.rattail_config.get_store(self.Session())
|
||||
|
||||
batch = self.model_class()
|
||||
batch.mode = mode
|
||||
batch.vendor = vendor
|
||||
batch.store = store
|
||||
batch.buyer = self.request.user.employee
|
||||
batch.created_by = self.request.user
|
||||
batch.po_total = 0
|
||||
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.ordering.view', uuid=batch.uuid))
|
||||
|
||||
data['index_title'] = self.get_index_title()
|
||||
data['index_url'] = self.get_index_url(mobile=True)
|
||||
data['mode_title'] = self.enum.PURCHASE_BATCH_MODE[mode].capitalize()
|
||||
|
||||
data['vendor_use_autocomplete'] = self.rattail_config.getbool(
|
||||
'rattail', 'vendor.use_autocomplete', default=True)
|
||||
if not data['vendor_use_autocomplete']:
|
||||
vendors = self.Session.query(model.Vendor)\
|
||||
.order_by(model.Vendor.name)
|
||||
options = [(tags.Option(vendor.name, vendor.uuid))
|
||||
for vendor in vendors]
|
||||
options.insert(0, tags.Option("(please choose)", ''))
|
||||
data['vendor_options'] = options
|
||||
|
||||
return self.render_to_response('create', data, mobile=True)
|
||||
|
||||
def configure_mobile_row_form(self, f):
|
||||
super(OrderingBatchView, self).configure_mobile_row_form(f)
|
||||
if self.editing:
|
||||
# TODO: probably should take `allow_cases` into account here...
|
||||
f.focus_spec = '[name="units_ordered"]'
|
||||
|
||||
def download_excel(self):
|
||||
"""
|
||||
Download ordering batch as Excel spreadsheet.
|
||||
|
|
|
@ -48,78 +48,11 @@ 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__)
|
||||
|
||||
|
||||
class MobileItemStatusFilter(grids.filters.MobileFilter):
|
||||
|
||||
value_choices = ['incomplete', 'unexpected', 'damaged', 'expired', 'all']
|
||||
|
||||
def filter_equal(self, query, value):
|
||||
|
||||
# NOTE: this is only relevant for truck dump or "from scratch"
|
||||
if value == 'received':
|
||||
return query.filter(sa.or_(
|
||||
model.PurchaseBatchRow.cases_received != 0,
|
||||
model.PurchaseBatchRow.units_received != 0))
|
||||
|
||||
if value == 'incomplete':
|
||||
# looking for any rows with "ordered" quantity, but where the
|
||||
# status does *not* signify a "settled" row so to speak
|
||||
# TODO: would be nice if we had a simple flag to leverage?
|
||||
return query.filter(sa.or_(model.PurchaseBatchRow.cases_ordered != 0,
|
||||
model.PurchaseBatchRow.units_ordered != 0))\
|
||||
.filter(~model.PurchaseBatchRow.status_code.in_((
|
||||
model.PurchaseBatchRow.STATUS_OK,
|
||||
model.PurchaseBatchRow.STATUS_PRODUCT_NOT_FOUND,
|
||||
model.PurchaseBatchRow.STATUS_CASE_QUANTITY_DIFFERS)))
|
||||
|
||||
if value == 'invalid':
|
||||
return query.filter(model.PurchaseBatchRow.status_code.in_((
|
||||
model.PurchaseBatchRow.STATUS_PRODUCT_NOT_FOUND,
|
||||
model.PurchaseBatchRow.STATUS_COST_NOT_FOUND,
|
||||
model.PurchaseBatchRow.STATUS_CASE_QUANTITY_UNKNOWN,
|
||||
model.PurchaseBatchRow.STATUS_CASE_QUANTITY_DIFFERS,
|
||||
)))
|
||||
|
||||
if value == 'unexpected':
|
||||
# looking for any rows which have "received" quantity but which
|
||||
# do *not* have any "ordered" quantity
|
||||
return query.filter(sa.and_(
|
||||
sa.or_(
|
||||
model.PurchaseBatchRow.cases_ordered == None,
|
||||
model.PurchaseBatchRow.cases_ordered == 0),
|
||||
sa.or_(
|
||||
model.PurchaseBatchRow.units_ordered == None,
|
||||
model.PurchaseBatchRow.units_ordered == 0),
|
||||
sa.or_(
|
||||
model.PurchaseBatchRow.cases_received != 0,
|
||||
model.PurchaseBatchRow.units_received != 0,
|
||||
model.PurchaseBatchRow.cases_damaged != 0,
|
||||
model.PurchaseBatchRow.units_damaged != 0,
|
||||
model.PurchaseBatchRow.cases_expired != 0,
|
||||
model.PurchaseBatchRow.units_expired != 0)))
|
||||
|
||||
if value == 'damaged':
|
||||
return query.filter(sa.or_(
|
||||
model.PurchaseBatchRow.cases_damaged != 0,
|
||||
model.PurchaseBatchRow.units_damaged != 0))
|
||||
|
||||
if value == 'expired':
|
||||
return query.filter(sa.or_(
|
||||
model.PurchaseBatchRow.cases_expired != 0,
|
||||
model.PurchaseBatchRow.units_expired != 0))
|
||||
|
||||
return query
|
||||
|
||||
def iter_choices(self):
|
||||
for value in self.value_choices:
|
||||
yield value, prettify(value)
|
||||
|
||||
|
||||
class ReceivingBatchView(PurchasingBatchView):
|
||||
"""
|
||||
Master view for receiving batches
|
||||
|
@ -132,11 +65,6 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
downloadable = True
|
||||
bulk_deletable = True
|
||||
rows_editable = True
|
||||
mobile_creatable = True
|
||||
mobile_rows_filterable = True
|
||||
mobile_rows_creatable = True
|
||||
mobile_rows_quickable = True
|
||||
mobile_rows_deletable = True
|
||||
|
||||
allow_from_po = False
|
||||
allow_from_scratch = True
|
||||
|
@ -207,11 +135,6 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
'executed_by',
|
||||
]
|
||||
|
||||
mobile_form_fields = [
|
||||
'vendor',
|
||||
'department',
|
||||
]
|
||||
|
||||
row_grid_columns = [
|
||||
'sequence',
|
||||
'upc',
|
||||
|
@ -295,20 +218,9 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
if batch.executed or batch.complete:
|
||||
return False
|
||||
|
||||
# can "always" delete rows from truck dump parent...
|
||||
# can always delete rows from truck dump parent
|
||||
if batch.is_truck_dump_parent():
|
||||
|
||||
# ...but only on desktop!
|
||||
if not self.mobile:
|
||||
return True
|
||||
|
||||
# ...for mobile we only allow deletion of rows which did *not* come
|
||||
# from a child batch, i.e. can delete ad-hoc rows only
|
||||
# TODO: should have a better way to detect this; for now we rely on
|
||||
# the fact that only rows from an invoice or similar would have
|
||||
# order quantities
|
||||
if not (row.cases_ordered or row.units_ordered):
|
||||
return True
|
||||
return True
|
||||
|
||||
# can always delete rows from truck dump child
|
||||
elif batch.is_truck_dump_child():
|
||||
|
@ -466,33 +378,32 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
kwargs['batch_vendor_map'] = vmap
|
||||
return kwargs
|
||||
|
||||
def get_batch_kwargs(self, batch, mobile=False):
|
||||
kwargs = super(ReceivingBatchView, self).get_batch_kwargs(batch, mobile=mobile)
|
||||
if not mobile:
|
||||
batch_type = self.request.POST['batch_type']
|
||||
if batch_type == 'from_scratch':
|
||||
kwargs.pop('truck_dump_batch', None)
|
||||
kwargs.pop('truck_dump_batch_uuid', None)
|
||||
elif batch_type == 'truck_dump_children_first':
|
||||
kwargs['truck_dump'] = True
|
||||
kwargs['truck_dump_children_first'] = True
|
||||
kwargs['order_quantities_known'] = True
|
||||
# TODO: this makes sense in some cases, but all?
|
||||
# (should just omit that field when not relevant)
|
||||
kwargs['date_ordered'] = None
|
||||
elif batch_type == 'truck_dump_children_last':
|
||||
kwargs['truck_dump'] = True
|
||||
kwargs['truck_dump_ready'] = True
|
||||
# TODO: this makes sense in some cases, but all?
|
||||
# (should just omit that field when not relevant)
|
||||
kwargs['date_ordered'] = None
|
||||
elif batch_type.startswith('truck_dump_child'):
|
||||
truck_dump = self.get_instance()
|
||||
kwargs['store'] = truck_dump.store
|
||||
kwargs['vendor'] = truck_dump.vendor
|
||||
kwargs['truck_dump_batch'] = truck_dump
|
||||
else:
|
||||
raise NotImplementedError
|
||||
def get_batch_kwargs(self, batch, **kwargs):
|
||||
kwargs = super(ReceivingBatchView, self).get_batch_kwargs(batch, **kwargs)
|
||||
batch_type = self.request.POST['batch_type']
|
||||
if batch_type == 'from_scratch':
|
||||
kwargs.pop('truck_dump_batch', None)
|
||||
kwargs.pop('truck_dump_batch_uuid', None)
|
||||
elif batch_type == 'truck_dump_children_first':
|
||||
kwargs['truck_dump'] = True
|
||||
kwargs['truck_dump_children_first'] = True
|
||||
kwargs['order_quantities_known'] = True
|
||||
# TODO: this makes sense in some cases, but all?
|
||||
# (should just omit that field when not relevant)
|
||||
kwargs['date_ordered'] = None
|
||||
elif batch_type == 'truck_dump_children_last':
|
||||
kwargs['truck_dump'] = True
|
||||
kwargs['truck_dump_ready'] = True
|
||||
# TODO: this makes sense in some cases, but all?
|
||||
# (should just omit that field when not relevant)
|
||||
kwargs['date_ordered'] = None
|
||||
elif batch_type.startswith('truck_dump_child'):
|
||||
truck_dump = self.get_instance()
|
||||
kwargs['store'] = truck_dump.store
|
||||
kwargs['vendor'] = truck_dump.vendor
|
||||
kwargs['truck_dump_batch'] = truck_dump
|
||||
else:
|
||||
raise NotImplementedError
|
||||
return kwargs
|
||||
|
||||
def department_for_purchase(self, purchase):
|
||||
|
@ -608,140 +519,6 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
url = self.request.route_url('receiving.view', uuid=truck_dump.uuid)
|
||||
return tags.link_to(text, url)
|
||||
|
||||
def render_mobile_listitem(self, batch, i):
|
||||
title = "({}) {} for ${:0,.2f} - {}, {}".format(
|
||||
batch.id_str,
|
||||
batch.vendor,
|
||||
batch.invoice_total or batch.po_total or 0,
|
||||
batch.department,
|
||||
batch.created_by)
|
||||
return title
|
||||
|
||||
def make_mobile_row_filters(self):
|
||||
"""
|
||||
Returns a set of filters for the mobile row grid.
|
||||
"""
|
||||
batch = self.get_instance()
|
||||
filters = grids.filters.GridFilterSet()
|
||||
|
||||
# visible filter options will depend on whether batch came from purchase
|
||||
if batch.order_quantities_known:
|
||||
value_choices = ['incomplete', 'unexpected', 'damaged', 'expired', 'invalid', 'all']
|
||||
default_status = 'incomplete'
|
||||
else:
|
||||
value_choices = ['received', 'damaged', 'expired', 'invalid', 'all']
|
||||
default_status = 'all'
|
||||
|
||||
# remove 'expired' filter option if not relevant
|
||||
if 'expired' in value_choices and not self.handler.allow_expired_credits():
|
||||
value_choices.remove('expired')
|
||||
|
||||
filters['status'] = MobileItemStatusFilter('status',
|
||||
value_choices=value_choices,
|
||||
default_value=default_status)
|
||||
return filters
|
||||
|
||||
def mobile_create(self):
|
||||
"""
|
||||
Mobile view for creating a new receiving batch
|
||||
"""
|
||||
mode = self.batch_mode
|
||||
data = {'mode': mode}
|
||||
phase = 1
|
||||
|
||||
schema = MobileNewReceivingBatch().bind(session=self.Session())
|
||||
form = forms.Form(schema=schema, request=self.request)
|
||||
if form.validate(newstyle=True):
|
||||
phase = form.validated['phase']
|
||||
|
||||
if form.validated['workflow'] == 'from_scratch':
|
||||
if not self.allow_from_scratch:
|
||||
raise NotImplementedError("Requested workflow not supported: from_scratch")
|
||||
batch = self.model_class()
|
||||
batch.store = self.rattail_config.get_store(self.Session())
|
||||
batch.mode = mode
|
||||
batch.vendor = self.Session.query(model.Vendor).get(form.validated['vendor'])
|
||||
batch.created_by = self.request.user
|
||||
batch.date_received = localtime(self.rattail_config).date()
|
||||
kwargs = self.get_batch_kwargs(batch, mobile=True)
|
||||
batch = self.handler.make_batch(self.Session(), **kwargs)
|
||||
return self.redirect(self.get_action_url('view', batch, mobile=True))
|
||||
|
||||
elif form.validated['workflow'] == 'truck_dump':
|
||||
if not self.allow_truck_dump:
|
||||
raise NotImplementedError("Requested workflow not supported: truck_dump")
|
||||
batch = self.model_class()
|
||||
batch.store = self.rattail_config.get_store(self.Session())
|
||||
batch.mode = mode
|
||||
batch.truck_dump = True
|
||||
batch.vendor = self.Session.query(model.Vendor).get(form.validated['vendor'])
|
||||
batch.created_by = self.request.user
|
||||
batch.date_received = localtime(self.rattail_config).date()
|
||||
kwargs = self.get_batch_kwargs(batch, mobile=True)
|
||||
batch = self.handler.make_batch(self.Session(), **kwargs)
|
||||
return self.redirect(self.get_action_url('view', batch, mobile=True))
|
||||
|
||||
elif form.validated['workflow'] == 'from_po':
|
||||
if not self.allow_from_po:
|
||||
raise NotImplementedError("Requested workflow not supported: from_po")
|
||||
|
||||
vendor = self.Session.query(model.Vendor).get(form.validated['vendor'])
|
||||
data['vendor'] = vendor
|
||||
|
||||
schema = self.make_mobile_receiving_from_po_schema()
|
||||
po_form = forms.Form(schema=schema, request=self.request)
|
||||
if phase == 2:
|
||||
if po_form.validate(newstyle=True):
|
||||
batch = self.model_class()
|
||||
batch.store = self.rattail_config.get_store(self.Session())
|
||||
batch.mode = mode
|
||||
batch.vendor = vendor
|
||||
batch.buyer = self.request.user.employee
|
||||
batch.created_by = self.request.user
|
||||
batch.date_received = localtime(self.rattail_config).date()
|
||||
self.assign_purchase_order(batch, po_form)
|
||||
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.get_action_url('view', batch, mobile=True))
|
||||
|
||||
else:
|
||||
phase = 2
|
||||
|
||||
else:
|
||||
raise NotImplementedError("Requested workflow not supported: {}".format(form.validated['workflow']))
|
||||
|
||||
data['form'] = form
|
||||
data['dform'] = form.make_deform_form()
|
||||
data['mode_title'] = self.enum.PURCHASE_BATCH_MODE[mode].capitalize()
|
||||
data['phase'] = phase
|
||||
|
||||
if phase == 1:
|
||||
data['vendor_use_autocomplete'] = self.rattail_config.getbool(
|
||||
'rattail', 'vendor.use_autocomplete', default=True)
|
||||
if not data['vendor_use_autocomplete']:
|
||||
vendors = self.Session.query(model.Vendor)\
|
||||
.order_by(model.Vendor.name)
|
||||
options = [(tags.Option(vendor.name, vendor.uuid))
|
||||
for vendor in vendors]
|
||||
options.insert(0, tags.Option("(please choose)", ''))
|
||||
data['vendor_options'] = options
|
||||
|
||||
elif phase == 2:
|
||||
purchases = self.eligible_purchases(vendor.uuid, mode=mode)
|
||||
data['purchases'] = [(p['key'], p['display']) for p in purchases['purchases']]
|
||||
data['purchase_order_fieldname'] = self.purchase_order_fieldname
|
||||
|
||||
return self.render_to_response('create', data, mobile=True)
|
||||
|
||||
def make_mobile_receiving_from_po_schema(self):
|
||||
schema = colander.MappingSchema()
|
||||
schema.add(colander.SchemaNode(colander.String(),
|
||||
name=self.purchase_order_fieldname,
|
||||
validator=self.validate_purchase))
|
||||
return schema.bind(session=self.Session())
|
||||
|
||||
@staticmethod
|
||||
@colander.deferred
|
||||
def validate_purchase(node, kw):
|
||||
|
@ -766,20 +543,6 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
if department:
|
||||
batch.department_uuid = department.uuid
|
||||
|
||||
def configure_mobile_form(self, f):
|
||||
super(ReceivingBatchView, self).configure_mobile_form(f)
|
||||
batch = f.model_instance
|
||||
|
||||
# truck_dump
|
||||
if not self.creating:
|
||||
if not batch.is_truck_dump_parent():
|
||||
f.remove_field('truck_dump')
|
||||
|
||||
# department
|
||||
if not self.creating:
|
||||
if batch.is_truck_dump_parent():
|
||||
f.remove_field('department')
|
||||
|
||||
def configure_row_grid(self, g):
|
||||
super(ReceivingBatchView, self).configure_row_grid(g)
|
||||
g.set_label('department_name', "Department")
|
||||
|
@ -858,7 +621,7 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
if row.product and row.product.is_pack_item():
|
||||
return self.get_row_action_url('transform_unit', row)
|
||||
|
||||
def receive_row(self, mobile=False):
|
||||
def receive_row(self, **kwargs):
|
||||
"""
|
||||
Primary desktop view for row-level receiving.
|
||||
"""
|
||||
|
@ -866,7 +629,6 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
# tries to pave the way for shared logic, i.e. where the latter would
|
||||
# simply invoke this method and return the result. however we're not
|
||||
# there yet...for now it's only tested for desktop
|
||||
self.mobile = mobile
|
||||
self.viewing = True
|
||||
row = self.get_row_instance()
|
||||
batch = row.batch
|
||||
|
@ -890,23 +652,14 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
'quick_receive_all': False,
|
||||
}
|
||||
|
||||
if mobile:
|
||||
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)
|
||||
|
||||
schema = ReceiveRowForm().bind(session=self.Session())
|
||||
form = forms.Form(schema=schema, request=self.request)
|
||||
form.cancel_url = self.get_row_action_url('view', row, mobile=mobile)
|
||||
form.cancel_url = self.get_row_action_url('view', row)
|
||||
form.set_widget('mode', forms.widgets.JQuerySelectWidget(values=[(m, m) for m in possible_modes]))
|
||||
form.set_widget('quantity', forms.widgets.CasesUnitsWidget(amount_required=True,
|
||||
one_amount_only=True))
|
||||
form.set_type('expiration_date', 'date_jquery')
|
||||
|
||||
if not mobile:
|
||||
form.remove_field('quick_receive')
|
||||
form.remove_field('quick_receive')
|
||||
|
||||
if form.validate(newstyle=True):
|
||||
|
||||
|
@ -921,20 +674,17 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
# whether or not it was 'CS' since the unit_uom can vary
|
||||
# TODO: should this be done for desktop too somehow?
|
||||
sticky_case = None
|
||||
if mobile and not form.validated['quick_receive']:
|
||||
cases = form.validated['cases']
|
||||
units = form.validated['units']
|
||||
if cases and not units:
|
||||
sticky_case = True
|
||||
elif units and not cases:
|
||||
sticky_case = False
|
||||
# if mobile and not form.validated['quick_receive']:
|
||||
# cases = form.validated['cases']
|
||||
# units = form.validated['units']
|
||||
# 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
|
||||
|
||||
if mobile:
|
||||
return self.redirect(self.get_action_url('view', batch, mobile=True))
|
||||
else:
|
||||
return self.redirect(self.get_row_action_url('view', row))
|
||||
return self.redirect(self.get_row_action_url('view', row))
|
||||
|
||||
# unit_uom can vary by product
|
||||
context['unit_uom'] = 'LB' if row.product and row.product.weighed else 'EA'
|
||||
|
@ -968,9 +718,9 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
# 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 = None
|
||||
if mobile:
|
||||
# TODO: should do this for desktop also, but rename the session variable
|
||||
sticky_case = self.request.session.get('tailbone.mobile.receiving.sticky_uom_is_case')
|
||||
# if mobile:
|
||||
# # TODO: should do this for desktop also, but rename the session variable
|
||||
# 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:
|
||||
|
@ -980,37 +730,37 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
if context['uom'] == 'CS' and row.units_ordered and not row.cases_ordered:
|
||||
context['uom'] = context['unit_uom']
|
||||
|
||||
# TODO: should do this for desktop in addition to mobile?
|
||||
if mobile and 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')
|
||||
# # TODO: should do this for desktop in addition to mobile?
|
||||
# if mobile and 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')
|
||||
|
||||
# TODO: should do this for desktop in addition to mobile?
|
||||
if mobile:
|
||||
# maybe alert user if they've already received some of this product
|
||||
alert_received = self.rattail_config.getbool('tailbone', 'receiving.alert_already_received',
|
||||
default=False)
|
||||
if alert_received:
|
||||
if self.handler.get_units_confirmed(row):
|
||||
msg = "You have already received some of this product; last update was {}.".format(
|
||||
humanize.naturaltime(make_utc() - row.modified))
|
||||
self.request.session.flash(msg, 'receiving-warning')
|
||||
# # TODO: should do this for desktop in addition to mobile?
|
||||
# if mobile:
|
||||
# # maybe alert user if they've already received some of this product
|
||||
# alert_received = self.rattail_config.getbool('tailbone', 'receiving.alert_already_received',
|
||||
# default=False)
|
||||
# if alert_received:
|
||||
# if self.handler.get_units_confirmed(row):
|
||||
# msg = "You have already received some of this product; last update was {}.".format(
|
||||
# humanize.naturaltime(make_utc() - row.modified))
|
||||
# self.request.session.flash(msg, 'receiving-warning')
|
||||
|
||||
context['form'] = form
|
||||
context['dform'] = form.make_deform_form()
|
||||
context['parent_url'] = self.get_action_url('view', batch, mobile=mobile)
|
||||
context['parent_url'] = self.get_action_url('view', batch)
|
||||
context['parent_title'] = self.get_instance_title(batch)
|
||||
return self.render_to_response('receive_row', context, mobile=mobile)
|
||||
return self.render_to_response('receive_row', context)
|
||||
|
||||
def declare_credit(self):
|
||||
"""
|
||||
|
@ -1418,8 +1168,8 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
self.Session.flush()
|
||||
return row
|
||||
|
||||
def redirect_after_edit_row(self, row, mobile=False):
|
||||
return self.redirect(self.get_row_action_url('view', row, mobile=mobile))
|
||||
def redirect_after_edit_row(self, row, **kwargs):
|
||||
return self.redirect(self.get_row_action_url('view', row))
|
||||
|
||||
def update_row_cost(self):
|
||||
"""
|
||||
|
@ -1463,287 +1213,16 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
},
|
||||
}
|
||||
|
||||
def render_mobile_row_listitem(self, row, i):
|
||||
key = self.render_product_key_value(row)
|
||||
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 save_quick_row_form(self, form):
|
||||
batch = self.get_instance()
|
||||
entry = form.validated['quick_entry']
|
||||
row = self.handler.quick_entry(self.Session(), batch, entry)
|
||||
return row
|
||||
|
||||
def redirect_after_quick_row(self, row, mobile=False):
|
||||
if mobile:
|
||||
return self.redirect(self.get_row_action_url('receive', row, mobile=mobile))
|
||||
return super(ReceivingBatchView, self).redirect_after_quick_row(row, mobile=mobile)
|
||||
|
||||
def get_row_image_url(self, row):
|
||||
if self.rattail_config.getbool('rattail.batch', 'purchase.mobile_images', default=True):
|
||||
return pod.get_image_url(self.rattail_config, row.upc)
|
||||
|
||||
def get_mobile_data(self, session=None):
|
||||
query = super(ReceivingBatchView, self).get_mobile_data(session=session)
|
||||
|
||||
# do not expose truck dump child batches on mobile
|
||||
# TODO: is there any case where we *would* want to?
|
||||
query = query.filter(model.PurchaseBatch.truck_dump_batch == None)
|
||||
|
||||
return query
|
||||
|
||||
def mobile_view_row(self):
|
||||
"""
|
||||
Mobile view for receiving batch row items. Note that this also handles
|
||||
updating a row.
|
||||
"""
|
||||
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)
|
||||
# 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']
|
||||
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('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)
|
||||
# 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']
|
||||
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')
|
||||
|
||||
# maybe alert user if they've already received some of this product
|
||||
alert_received = self.rattail_config.getbool('tailbone', 'receiving.alert_already_received',
|
||||
default=False)
|
||||
if alert_received:
|
||||
if self.handler.get_units_confirmed(row):
|
||||
msg = "You have already received some of this product; last update was {}.".format(
|
||||
humanize.naturaltime(make_utc() - row.modified))
|
||||
self.request.session.flash(msg, '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
|
||||
|
@ -1804,16 +1283,11 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
instance_url_prefix = cls.get_instance_url_prefix()
|
||||
model_key = cls.get_model_key()
|
||||
permission_prefix = cls.get_permission_prefix()
|
||||
legacy_mobile = cls.legacy_mobile_enabled(rattail_config)
|
||||
|
||||
# row-level receiving
|
||||
config.add_route('{}.receive_row'.format(route_prefix), '{}/{{uuid}}/rows/{{row_uuid}}/receive'.format(url_prefix))
|
||||
config.add_view(cls, attr='receive_row', route_name='{}.receive_row'.format(route_prefix),
|
||||
permission='{}.edit_row'.format(permission_prefix))
|
||||
if legacy_mobile:
|
||||
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))
|
||||
|
||||
# declare credit for row
|
||||
config.add_route('{}.declare_credit'.format(route_prefix), '{}/{{uuid}}/rows/{{row_uuid}}/declare-credit'.format(url_prefix))
|
||||
|
@ -1854,40 +1328,6 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
cls._defaults(config)
|
||||
|
||||
|
||||
# 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_vendor(node, kw):
|
||||
session = kw['session']
|
||||
def validate(node, value):
|
||||
vendor = session.query(model.Vendor).get(value)
|
||||
if not vendor:
|
||||
raise colander.Invalid(node, "Vendor not found")
|
||||
return vendor.uuid
|
||||
return validate
|
||||
|
||||
|
||||
class MobileNewReceivingBatch(colander.MappingSchema):
|
||||
|
||||
vendor = colander.SchemaNode(colander.String(),
|
||||
validator=valid_vendor)
|
||||
|
||||
workflow = colander.SchemaNode(colander.String(),
|
||||
validator=colander.OneOf([
|
||||
'from_po',
|
||||
'from_scratch',
|
||||
'truck_dump',
|
||||
]))
|
||||
|
||||
phase = colander.SchemaNode(colander.Int())
|
||||
|
||||
|
||||
class MobileNewReceivingFromPO(colander.MappingSchema):
|
||||
|
||||
purchase = colander.SchemaNode(colander.String())
|
||||
|
||||
|
||||
class ReceiveRowForm(colander.MappingSchema):
|
||||
|
||||
mode = colander.SchemaNode(colander.String(),
|
||||
|
|
Loading…
Reference in a new issue