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:
Lance Edgar 2021-01-30 15:52:47 -06:00
parent fac00e6ecd
commit 708641a8f1
70 changed files with 196 additions and 4886 deletions

View file

@ -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')

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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',

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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))

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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):

View file

@ -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.

View file

@ -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(),