Improve support for "receive from scratch" workflow, esp. for mobile
also try harder to make certain aspects easier to enable/disable via handler, e.g. whether cases should be allowed as quantity input, or expired credits should be a thing etc.
This commit is contained in:
parent
a34a42d2b2
commit
d8b45db331
|
@ -27,9 +27,9 @@ $(document).on('click', 'form[name="new-purchasing-batch"] #receive-truck-dump',
|
|||
});
|
||||
|
||||
|
||||
$(document).on('click', 'form.receiving-update #delete-receiving-row', function() {
|
||||
$(document).on('click', 'form[name="new-purchasing-batch"] #receive-from-scratch', function() {
|
||||
var form = $(this).parents('form');
|
||||
form.find('input[name="delete_row"]').val('true');
|
||||
form.find('input[name="workflow"]').val('from_scratch');
|
||||
form.submit();
|
||||
});
|
||||
|
||||
|
@ -68,11 +68,15 @@ $(document).on('click', 'form.receiving-update .receiving-actions button', funct
|
|||
});
|
||||
|
||||
|
||||
// quick-receive (1 CS)
|
||||
$(document).on('click', 'form.receiving-update .receive-one-case', function() {
|
||||
// quick-receive (1 case or unit)
|
||||
$(document).on('click', 'form.receiving-update .quick-receive', function() {
|
||||
var form = $(this).parents('form:first');
|
||||
form.find('[name="mode"]').val('received');
|
||||
if ($(this).data('uom') == 'CS') {
|
||||
form.find('[name="cases"]').val('1');
|
||||
} else {
|
||||
form.find('[name="units"]').val('1');
|
||||
}
|
||||
form.find('input[name="quick_receive"]').val('true');
|
||||
form.submit();
|
||||
});
|
||||
|
|
|
@ -149,7 +149,7 @@ def context_found(event):
|
|||
return False
|
||||
request.has_any_perm = has_any_perm
|
||||
|
||||
def get_referrer(default=None):
|
||||
def get_referrer(default=None, mobile=False):
|
||||
if request.params.get('referrer'):
|
||||
return request.params['referrer']
|
||||
if request.session.get('referrer'):
|
||||
|
@ -157,7 +157,12 @@ def context_found(event):
|
|||
referrer = request.referrer
|
||||
if (not referrer or referrer == request.current_route_url()
|
||||
or not referrer.startswith(request.host_url)):
|
||||
referrer = default or request.route_url('home')
|
||||
if default:
|
||||
referrer = default
|
||||
elif mobile:
|
||||
referrer = request.route_url('mobile.home')
|
||||
else:
|
||||
referrer = request.route_url('home')
|
||||
return referrer
|
||||
request.get_referrer = get_referrer
|
||||
|
||||
|
|
|
@ -16,3 +16,10 @@
|
|||
## ${form.render(buttons=capture(self.buttons))|n}
|
||||
${form.render()|n}
|
||||
</div><!-- form-wrapper -->
|
||||
|
||||
% if master.mobile_rows_deletable and request.has_perm('{}.delete_row'.format(permission_prefix)):
|
||||
${h.form(url('mobile.{}.delete_row'.format(route_prefix), uuid=parent_instance.uuid, row_uuid=row.uuid))}
|
||||
${h.csrf_token(request)}
|
||||
${h.submit('submit', "Delete this Row")}
|
||||
${h.end_form()}
|
||||
% endif
|
||||
|
|
|
@ -34,7 +34,7 @@ ${form.render()|n}
|
|||
<button type="button" style="display: none;">Change</button>
|
||||
</div>
|
||||
% else:
|
||||
${h.text('quick_row_entry', placeholder=placeholder, autocomplete='off', **{'data-type': 'search', 'data-url': url('mobile.{}.quick_row'.format(route_prefix), uuid=instance.uuid), 'data-wedge': quick_row_keyboard_wedge})}
|
||||
${h.text('quick_row_entry', placeholder=placeholder, autocomplete='off', **{'data-type': 'search', 'data-url': url('mobile.{}.quick_row'.format(route_prefix), uuid=instance.uuid), 'data-wedge': 'true' if quick_row_keyboard_wedge else 'false'})}
|
||||
% endif
|
||||
${h.end_form()}
|
||||
% endif
|
||||
|
|
|
@ -10,3 +10,10 @@ ${form.render()|n}
|
|||
% if master.mobile_rows_editable and instance_editable and request.has_perm('{}.edit_row'.format(permission_prefix)):
|
||||
${h.link_to("Edit", url('mobile.{}.edit_row'.format(route_prefix), uuid=instance.batch_uuid, row_uuid=instance.uuid), class_='ui-btn')}
|
||||
% endif
|
||||
|
||||
% if master.mobile_rows_deletable and request.has_perm('{}.delete_row'.format(permission_prefix)):
|
||||
${h.form(url('mobile.{}.delete_row'.format(route_prefix), uuid=parent_instance.uuid, row_uuid=row.uuid))}
|
||||
${h.csrf_token(request)}
|
||||
${h.submit('submit', "Delete this Row")}
|
||||
${h.end_form()}
|
||||
% endif
|
||||
|
|
|
@ -31,7 +31,7 @@ ${h.csrf_token(request)}
|
|||
% endif
|
||||
|
||||
% if master.allow_from_scratch:
|
||||
<button type="button">Receive from Scratch</button>
|
||||
<button type="button" id="receive-from-scratch">Receive from Scratch</button>
|
||||
% endif
|
||||
|
||||
% if master.allow_truck_dump:
|
||||
|
|
|
@ -11,37 +11,63 @@
|
|||
<div class="ui-block-a">
|
||||
% if instance.product:
|
||||
<h3>${instance.brand_name or ""}</h3>
|
||||
<h3>${instance.description} ${instance.size}</h3>
|
||||
<h3>1 CS = ${h.pretty_quantity(row.case_quantity)} ${unit_uom}</h3>
|
||||
<h3>${instance.description} ${instance.size or ''}</h3>
|
||||
% if allow_cases:
|
||||
<h3>1 CS = ${h.pretty_quantity(row.case_quantity or 1)} ${unit_uom}</h3>
|
||||
% endif
|
||||
% else:
|
||||
<h3>${instance.description}</h3>
|
||||
% endif
|
||||
</div>
|
||||
<div class="ui-block-b">
|
||||
% if product_image_url:
|
||||
${h.image(product_image_url, "product image")}
|
||||
% endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
% if not batch.truck_dump:
|
||||
% if populated_from_purchase:
|
||||
<tr>
|
||||
<td>ordered</td>
|
||||
<td>${h.pretty_quantity(row.cases_ordered or 0)} / ${h.pretty_quantity(row.units_ordered or 0)}</td>
|
||||
<td>
|
||||
% if allow_cases:
|
||||
${h.pretty_quantity(row.cases_ordered or 0)} /
|
||||
% endif
|
||||
${h.pretty_quantity(row.units_ordered or 0)}
|
||||
</td>
|
||||
</tr>
|
||||
% endif
|
||||
<tr>
|
||||
<td>received</td>
|
||||
<td>${h.pretty_quantity(row.cases_received or 0)} / ${h.pretty_quantity(row.units_received or 0)}</td>
|
||||
<td>
|
||||
% if allow_cases:
|
||||
${h.pretty_quantity(row.cases_received or 0)} /
|
||||
% endif
|
||||
${h.pretty_quantity(row.units_received or 0)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>damaged</td>
|
||||
<td>${h.pretty_quantity(row.cases_damaged or 0)} / ${h.pretty_quantity(row.units_damaged or 0)}</td>
|
||||
<td>
|
||||
% if allow_cases:
|
||||
${h.pretty_quantity(row.cases_damaged or 0)} /
|
||||
% endif
|
||||
${h.pretty_quantity(row.units_damaged or 0)}
|
||||
</td>
|
||||
</tr>
|
||||
% if allow_expired:
|
||||
<tr>
|
||||
<td>expired</td>
|
||||
<td>${h.pretty_quantity(row.cases_expired or 0)} / ${h.pretty_quantity(row.units_expired or 0)}</td>
|
||||
<td>
|
||||
% if allow_cases:
|
||||
${h.pretty_quantity(row.cases_expired or 0)} /
|
||||
% endif
|
||||
${h.pretty_quantity(row.units_expired or 0)}
|
||||
</td>
|
||||
</tr>
|
||||
% endif
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
@ -59,9 +85,13 @@
|
|||
${h.hidden('cases')}
|
||||
${h.hidden('units')}
|
||||
|
||||
<button type="button" class="receive-one-case">Receive 1 CS</button>
|
||||
% if allow_cases:
|
||||
<button type="button" class="quick-receive" data-uom="CS">Receive 1 CS</button>
|
||||
% else:
|
||||
<button type="button" class="quick-receive" data-uom="${unit_uom}">Receive 1 ${unit_uom}</button>
|
||||
% endif
|
||||
|
||||
${keypad(unit_uom, uom)}
|
||||
${keypad(unit_uom, uom, allow_cases=allow_cases)}
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
|
@ -70,7 +100,9 @@
|
|||
<fieldset data-role="controlgroup" data-type="horizontal" class="receiving-mode">
|
||||
${h.radio('mode', value='received', label="received", checked=True)}
|
||||
${h.radio('mode', value='damaged', label="damaged")}
|
||||
% if allow_expired:
|
||||
${h.radio('mode', value='expired', label="expired")}
|
||||
% endif
|
||||
</fieldset>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -95,11 +127,13 @@
|
|||
</table>
|
||||
|
||||
${h.hidden('quick_receive', value='false')}
|
||||
${h.end_form()}
|
||||
|
||||
${h.hidden('delete_row', value='false')}
|
||||
% if request.has_perm('{}.delete_row'.format(permission_prefix)):
|
||||
<button type="button" id="delete-receiving-row">Delete this Row</button>
|
||||
% endif
|
||||
|
||||
% if master.mobile_rows_deletable and request.has_perm('{}.delete_row'.format(permission_prefix)):
|
||||
${h.form(url('mobile.{}.delete_row'.format(route_prefix), uuid=batch.uuid, row_uuid=row.uuid), class_='receiving-update')}
|
||||
${h.csrf_token(request)}
|
||||
${h.submit('submit', "Delete this Row")}
|
||||
${h.end_form()}
|
||||
% endif
|
||||
|
||||
% endif
|
||||
|
|
|
@ -48,7 +48,6 @@ from rattail.progress import SocketProgress
|
|||
|
||||
import colander
|
||||
import deform
|
||||
from pyramid import httpexceptions
|
||||
from pyramid.renderers import render_to_response
|
||||
from pyramid.response import FileResponse
|
||||
from webhelpers2.html import HTML, tags
|
||||
|
@ -1074,20 +1073,11 @@ class BatchMasterView(MasterView):
|
|||
def get_parent(self, row):
|
||||
return row.batch
|
||||
|
||||
def delete_row(self):
|
||||
def delete_row_object(self, row):
|
||||
"""
|
||||
"Delete" a row from the batch. This sets the ``removed`` flag on the
|
||||
row but does not truly delete it.
|
||||
Perform the actual deletion of given row object.
|
||||
"""
|
||||
row = self.Session.query(self.model_row_class).get(self.request.matchdict['row_uuid'])
|
||||
if not row:
|
||||
raise httpexceptions.HTTPNotFound()
|
||||
row.removed = True
|
||||
batch = self.get_parent(row)
|
||||
self.handler.refresh_batch_status(batch)
|
||||
if batch.rowcount is not None:
|
||||
batch.rowcount -= 1
|
||||
return self.redirect(self.get_action_url('view', batch))
|
||||
self.handler.remove_row(row)
|
||||
|
||||
def bulk_delete_rows(self):
|
||||
"""
|
||||
|
@ -1096,9 +1086,14 @@ class BatchMasterView(MasterView):
|
|||
"""
|
||||
batch = self.get_instance()
|
||||
query = self.get_effective_row_data(sort=False)
|
||||
|
||||
# TODO: this should surely be handled by the handler...
|
||||
if batch.rowcount is not None:
|
||||
batch.rowcount -= query.count()
|
||||
query.update({'removed': True}, synchronize_session=False)
|
||||
self.Session.refresh(batch)
|
||||
self.handler.refresh_batch_status(batch)
|
||||
|
||||
return self.redirect(self.get_action_url('view', batch))
|
||||
|
||||
def execute(self):
|
||||
|
|
|
@ -139,6 +139,7 @@ class MasterView(View):
|
|||
mobile_rows_filterable = False
|
||||
mobile_rows_viewable = False
|
||||
mobile_rows_editable = False
|
||||
mobile_rows_deletable = False
|
||||
|
||||
row_labels = {}
|
||||
|
||||
|
@ -2670,11 +2671,12 @@ class MasterView(View):
|
|||
|
||||
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_instance': parent,
|
||||
'parent_title': self.get_instance_title(parent),
|
||||
'parent_url': self.get_action_url('view', parent, mobile=True),
|
||||
'form': form},
|
||||
|
@ -2705,16 +2707,38 @@ class MasterView(View):
|
|||
"""
|
||||
return True
|
||||
|
||||
def delete_row_object(self, row):
|
||||
"""
|
||||
Perform the actual deletion of given row object.
|
||||
"""
|
||||
self.Session.delete(row)
|
||||
|
||||
def delete_row(self):
|
||||
"""
|
||||
"Delete" a sub-row from the parent.
|
||||
Desktop view which can "delete" a sub-row from the parent.
|
||||
"""
|
||||
row = self.Session.query(self.model_row_class).get(self.request.matchdict['row_uuid'])
|
||||
if not row:
|
||||
raise httpexceptions.HTTPNotFound()
|
||||
self.Session.delete(row)
|
||||
raise self.notfound()
|
||||
self.delete_row_object(row)
|
||||
return self.redirect(self.get_action_url('edit', 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
|
||||
|
||||
|
@ -3050,9 +3074,15 @@ class MasterView(View):
|
|||
permission='{}.edit_row'.format(permission_prefix))
|
||||
|
||||
# delete row
|
||||
if cls.has_rows and cls.rows_deletable:
|
||||
if cls.has_rows:
|
||||
if cls.rows_deletable or cls.mobile_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))
|
||||
config.add_tailbone_permission(permission_prefix, '{}.delete_row'.format(permission_prefix),
|
||||
"Delete individual {} rows".format(model_title))
|
||||
if 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))
|
||||
|
|
|
@ -160,16 +160,23 @@ class PurchaseView(MasterView):
|
|||
default_active=True, default_verb='contains')
|
||||
g.sorters['buyer'] = g.make_sorter(model.Person.display_name)
|
||||
|
||||
g.filters['date_ordered'].label = "Ordered"
|
||||
# date_ordered
|
||||
g.filters['date_ordered'].default_active = True
|
||||
g.filters['date_ordered'].default_verb = 'equal'
|
||||
|
||||
g.set_label('date_ordered', "Ordered")
|
||||
g.set_sort_defaults('date_ordered', 'desc')
|
||||
|
||||
g.set_enum('status', self.enum.PURCHASE_STATUS)
|
||||
|
||||
g.set_label('date_ordered', "Ordered")
|
||||
# date_received
|
||||
g.filters['date_received'].default_active = True
|
||||
g.filters['date_received'].default_verb = 'equal'
|
||||
g.set_label('date_received', "Received")
|
||||
|
||||
# status
|
||||
g.set_enum('status', self.enum.PURCHASE_STATUS)
|
||||
g.filters['status'].default_active = True
|
||||
g.filters['status'].verbs = ['equal', 'not_equal', 'is_any']
|
||||
g.filters['status'].default_verb = 'is_any'
|
||||
|
||||
g.set_label('invoice_number', "Invoice No.")
|
||||
|
||||
def configure_form(self, f):
|
||||
|
|
|
@ -55,6 +55,7 @@ class OrderingBatchView(PurchasingBatchView):
|
|||
mobile_rows_creatable = True
|
||||
mobile_rows_quickable = True
|
||||
mobile_rows_editable = True
|
||||
mobile_rows_deletable = True
|
||||
has_worksheet = True
|
||||
|
||||
mobile_form_fields = [
|
||||
|
@ -186,7 +187,7 @@ class OrderingBatchView(PurchasingBatchView):
|
|||
'history': history,
|
||||
'get_upc': lambda p: p.upc.pretty() if p.upc else '',
|
||||
'header_columns': self.order_form_header_columns,
|
||||
'ignore_cases': self.handler.ignore_cases,
|
||||
'ignore_cases': not self.handler.allow_cases(),
|
||||
})
|
||||
|
||||
def get_order_form_history(self, batch, costs, count):
|
||||
|
|
|
@ -27,12 +27,14 @@ Views for 'receiving' (purchasing) batches
|
|||
from __future__ import unicode_literals, absolute_import
|
||||
|
||||
import re
|
||||
import logging
|
||||
|
||||
import six
|
||||
import sqlalchemy as sa
|
||||
|
||||
from rattail import pod
|
||||
from rattail.db import model, api
|
||||
from rattail.db.util import maxlen
|
||||
from rattail.gpc import GPC
|
||||
from rattail.time import localtime
|
||||
from rattail.util import pretty_quantity, prettify, OrderedDict
|
||||
|
@ -47,13 +49,16 @@ from tailbone import forms, grids
|
|||
from tailbone.views.purchasing import PurchasingBatchView
|
||||
|
||||
|
||||
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
|
||||
# NOTE: this is only relevant for truck dump or "from scratch"
|
||||
if value == 'received':
|
||||
return query.filter(sa.or_(
|
||||
model.PurchaseBatchRow.cases_received != 0,
|
||||
|
@ -105,6 +110,7 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
mobile_rows_filterable = True
|
||||
mobile_rows_creatable = True
|
||||
mobile_rows_quickable = True
|
||||
mobile_rows_deletable = True
|
||||
|
||||
allow_from_po = False
|
||||
allow_from_scratch = True
|
||||
|
@ -354,6 +360,7 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
kwargs = super(ReceivingBatchView, self).get_batch_kwargs(batch, mobile=mobile)
|
||||
|
||||
if mobile:
|
||||
if 'purchase' in self.request.POST:
|
||||
purchase = self.get_purchase(self.request.POST['purchase'])
|
||||
if isinstance(purchase, model.Purchase):
|
||||
kwargs['purchase'] = purchase
|
||||
|
@ -501,12 +508,19 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
"""
|
||||
batch = self.get_instance()
|
||||
filters = grids.filters.GridFilterSet()
|
||||
if batch.truck_dump:
|
||||
value_choices = ['received', 'damaged', 'expired', 'all']
|
||||
default_status = 'all'
|
||||
else:
|
||||
|
||||
# visible filter options will depend on whether batch came from purchase
|
||||
if self.handler.populated_from_purchase(batch):
|
||||
value_choices = ['incomplete', 'unexpected', 'damaged', 'expired', 'all']
|
||||
default_status = 'incomplete'
|
||||
else:
|
||||
value_choices = ['received', 'damaged', 'expired', '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)
|
||||
|
@ -522,10 +536,24 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
mode = self.batch_mode
|
||||
data = {'mode': mode}
|
||||
|
||||
form = forms.Form(schema=MobileNewReceivingBatch(), request=self.request)
|
||||
schema = MobileNewReceivingBatch().bind(session=self.Session())
|
||||
form = forms.Form(schema=schema, request=self.request)
|
||||
if form.validate(newstyle=True):
|
||||
|
||||
if form.validated['workflow'] == 'truck_dump':
|
||||
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.request.route_url('mobile.receiving.view', uuid=batch.uuid))
|
||||
|
||||
elif form.validated['workflow'] == 'truck_dump':
|
||||
if not self.allow_truck_dump:
|
||||
raise NotImplementedError("Requested workflow not supported: truck_dump")
|
||||
batch = self.model_class()
|
||||
|
@ -611,21 +639,35 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
def quick_locate_rows(self, batch, entry):
|
||||
rows = []
|
||||
|
||||
# we prefer "exact" matches, i.e. those which assumed the entry already
|
||||
# contained the check digit.
|
||||
provided = GPC(entry, calc_check_digit=False)
|
||||
for row in batch.active_rows():
|
||||
if row.upc == provided:
|
||||
rows.append(row)
|
||||
# try to locate rows by product uuid match before other key
|
||||
product = self.Session.query(model.Product).get(entry)
|
||||
if product:
|
||||
rows = [row for row in batch.active_rows()
|
||||
if row.product_uuid == product.uuid]
|
||||
if rows:
|
||||
return rows
|
||||
|
||||
# if no "exact" matches, we'll settle for those which assume the entry
|
||||
# lacked a check digit.
|
||||
key = self.rattail_config.product_key()
|
||||
if key == 'upc':
|
||||
|
||||
# we prefer "exact" UPC matches, i.e. those which assumed the entry
|
||||
# already contained the check digit.
|
||||
provided = GPC(entry, calc_check_digit=False)
|
||||
rows = [row for row in batch.active_rows()
|
||||
if row.upc == provided]
|
||||
if rows:
|
||||
return rows
|
||||
|
||||
# if no "exact" UPC matches, we'll settle for those (UPC matches)
|
||||
# which assume the entry lacked a check digit.
|
||||
checked = GPC(entry, calc_check_digit='upc')
|
||||
for row in batch.active_rows():
|
||||
if row.upc == checked:
|
||||
rows.append(row)
|
||||
rows = [row for row in batch.active_rows()
|
||||
if row.upc == checked]
|
||||
return rows
|
||||
|
||||
elif key == 'item_id':
|
||||
rows = [row for row in batch.active_rows()
|
||||
if row.item_id == entry]
|
||||
return rows
|
||||
|
||||
def save_quick_row_form(self, form):
|
||||
|
@ -652,6 +694,18 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
self.Session.flush()
|
||||
return row
|
||||
|
||||
# try to locate product by uuid before other, more specific key
|
||||
product = self.Session.query(model.Product).get(entry)
|
||||
if product and not product.deleted:
|
||||
row = model.PurchaseBatchRow()
|
||||
row.product = product
|
||||
self.handler.add_row(batch, row)
|
||||
self.Session.flush()
|
||||
return row
|
||||
|
||||
key = self.rattail_config.product_key()
|
||||
if key == 'upc':
|
||||
|
||||
# try to locate product by upc
|
||||
provided = GPC(entry, calc_check_digit=False)
|
||||
checked = GPC(entry, calc_check_digit='upc')
|
||||
|
@ -672,11 +726,38 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
# product not in system, but presumably sane upc, so add to batch anyway
|
||||
row = model.PurchaseBatchRow()
|
||||
row.upc = provided # TODO: why not checked? how to know?
|
||||
row.item_id = entry
|
||||
row.description = "(unknown product)"
|
||||
self.handler.add_row(batch, row)
|
||||
self.Session.flush()
|
||||
return row
|
||||
|
||||
elif key == 'item_id':
|
||||
|
||||
# try to locate product by item_id
|
||||
product = api.get_product_by_item_id(self.Session(), entry)
|
||||
if product:
|
||||
row = model.PurchaseBatchRow()
|
||||
row.product = product
|
||||
self.handler.add_row(batch, row)
|
||||
self.Session.flush()
|
||||
return row
|
||||
|
||||
# check for "too long" item_id
|
||||
if len(entry) > maxlen(model.PurchaseBatchRow.item_id):
|
||||
return
|
||||
|
||||
# product not in system, but presumably sane item_id, so add to batch anyway
|
||||
row = model.PurchaseBatchRow()
|
||||
row.item_id = entry
|
||||
row.description = "(unknown product)"
|
||||
self.handler.add_row(batch, row)
|
||||
self.Session.flush()
|
||||
return row
|
||||
|
||||
else:
|
||||
raise NotImplementedError("don't know how to handle product key: {}".format(key))
|
||||
|
||||
def redirect_after_quick_row(self, row, mobile=False):
|
||||
if mobile:
|
||||
return self.redirect(self.get_row_action_url('view', row, mobile=mobile))
|
||||
|
@ -695,11 +776,15 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
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': pod.get_image_url(self.rattail_config, row.upc),
|
||||
'form': form,
|
||||
'populated_from_purchase': self.handler.populated_from_purchase(batch),
|
||||
'allow_expired': self.handler.allow_expired_credits(),
|
||||
'allow_cases': self.handler.allow_cases(),
|
||||
}
|
||||
|
||||
if self.request.has_perm('{}.create_row'.format(permission_prefix)):
|
||||
|
@ -707,15 +792,6 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
update_form = forms.Form(schema=schema, request=self.request)
|
||||
if update_form.validate(newstyle=True):
|
||||
row = self.Session.query(model.PurchaseBatchRow).get(update_form.validated['row'])
|
||||
|
||||
# TODO: surely this (delete_row) should be split out to a separate view
|
||||
if update_form.validated['delete_row']:
|
||||
if not self.request.has_perm('{}.delete_row'.format(permission_prefix)):
|
||||
raise httpexceptions.HTTPForbidden()
|
||||
self.handler.remove_row(row)
|
||||
return self.redirect(self.get_action_url('view', batch, mobile=True))
|
||||
|
||||
else: # not delete_row
|
||||
mode = update_form.validated['mode']
|
||||
cases = update_form.validated['cases']
|
||||
units = update_form.validated['units']
|
||||
|
@ -773,7 +849,7 @@ class ReceivingBatchView(PurchasingBatchView):
|
|||
if context['uom'] == 'CS' and row.units_ordered and not row.cases_ordered:
|
||||
context['uom'] = context['unit_uom']
|
||||
|
||||
if not row.cases_ordered and not row.units_ordered and not batch.truck_dump:
|
||||
if self.handler.populated_from_purchase(batch) and not row.cases_ordered and not row.units_ordered:
|
||||
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)
|
||||
|
||||
|
@ -844,9 +920,24 @@ 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(forms.types.VendorType())
|
||||
vendor = colander.SchemaNode(colander.String(),
|
||||
validator=valid_vendor)
|
||||
|
||||
workflow = colander.SchemaNode(colander.String(),
|
||||
validator=colander.OneOf([
|
||||
|
@ -895,8 +986,6 @@ class MobileReceivingForm(colander.MappingSchema):
|
|||
|
||||
quick_receive = colander.SchemaNode(colander.Boolean())
|
||||
|
||||
delete_row = colander.SchemaNode(colander.Boolean())
|
||||
|
||||
|
||||
def includeme(config):
|
||||
ReceivingBatchView.defaults(config)
|
||||
|
|
|
@ -40,6 +40,8 @@ class SettingsView(MasterView):
|
|||
Master view for the settings model.
|
||||
"""
|
||||
model_class = model.Setting
|
||||
model_title = "Raw Setting"
|
||||
model_title_plural = "Raw Settings"
|
||||
feedback = re.compile(r'^rattail\.mail\.user_feedback\..*')
|
||||
|
||||
grid_columns = [
|
||||
|
|
Loading…
Reference in a new issue