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');
|
var form = $(this).parents('form');
|
||||||
form.find('input[name="delete_row"]').val('true');
|
form.find('input[name="workflow"]').val('from_scratch');
|
||||||
form.submit();
|
form.submit();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -68,11 +68,15 @@ $(document).on('click', 'form.receiving-update .receiving-actions button', funct
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// quick-receive (1 CS)
|
// quick-receive (1 case or unit)
|
||||||
$(document).on('click', 'form.receiving-update .receive-one-case', function() {
|
$(document).on('click', 'form.receiving-update .quick-receive', function() {
|
||||||
var form = $(this).parents('form:first');
|
var form = $(this).parents('form:first');
|
||||||
form.find('[name="mode"]').val('received');
|
form.find('[name="mode"]').val('received');
|
||||||
form.find('[name="cases"]').val('1');
|
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.find('input[name="quick_receive"]').val('true');
|
||||||
form.submit();
|
form.submit();
|
||||||
});
|
});
|
||||||
|
|
|
@ -149,7 +149,7 @@ def context_found(event):
|
||||||
return False
|
return False
|
||||||
request.has_any_perm = has_any_perm
|
request.has_any_perm = has_any_perm
|
||||||
|
|
||||||
def get_referrer(default=None):
|
def get_referrer(default=None, mobile=False):
|
||||||
if request.params.get('referrer'):
|
if request.params.get('referrer'):
|
||||||
return request.params['referrer']
|
return request.params['referrer']
|
||||||
if request.session.get('referrer'):
|
if request.session.get('referrer'):
|
||||||
|
@ -157,7 +157,12 @@ def context_found(event):
|
||||||
referrer = request.referrer
|
referrer = request.referrer
|
||||||
if (not referrer or referrer == request.current_route_url()
|
if (not referrer or referrer == request.current_route_url()
|
||||||
or not referrer.startswith(request.host_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
|
return referrer
|
||||||
request.get_referrer = get_referrer
|
request.get_referrer = get_referrer
|
||||||
|
|
||||||
|
|
|
@ -16,3 +16,10 @@
|
||||||
## ${form.render(buttons=capture(self.buttons))|n}
|
## ${form.render(buttons=capture(self.buttons))|n}
|
||||||
${form.render()|n}
|
${form.render()|n}
|
||||||
</div><!-- form-wrapper -->
|
</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>
|
<button type="button" style="display: none;">Change</button>
|
||||||
</div>
|
</div>
|
||||||
% else:
|
% 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
|
% endif
|
||||||
${h.end_form()}
|
${h.end_form()}
|
||||||
% endif
|
% 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)):
|
% 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')}
|
${h.link_to("Edit", url('mobile.{}.edit_row'.format(route_prefix), uuid=instance.batch_uuid, row_uuid=instance.uuid), class_='ui-btn')}
|
||||||
% endif
|
% 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
|
% endif
|
||||||
|
|
||||||
% if master.allow_from_scratch:
|
% if master.allow_from_scratch:
|
||||||
<button type="button">Receive from Scratch</button>
|
<button type="button" id="receive-from-scratch">Receive from Scratch</button>
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
% if master.allow_truck_dump:
|
% if master.allow_truck_dump:
|
||||||
|
|
|
@ -11,37 +11,63 @@
|
||||||
<div class="ui-block-a">
|
<div class="ui-block-a">
|
||||||
% if instance.product:
|
% if instance.product:
|
||||||
<h3>${instance.brand_name or ""}</h3>
|
<h3>${instance.brand_name or ""}</h3>
|
||||||
<h3>${instance.description} ${instance.size}</h3>
|
<h3>${instance.description} ${instance.size or ''}</h3>
|
||||||
<h3>1 CS = ${h.pretty_quantity(row.case_quantity)} ${unit_uom}</h3>
|
% if allow_cases:
|
||||||
|
<h3>1 CS = ${h.pretty_quantity(row.case_quantity or 1)} ${unit_uom}</h3>
|
||||||
|
% endif
|
||||||
% else:
|
% else:
|
||||||
<h3>${instance.description}</h3>
|
<h3>${instance.description}</h3>
|
||||||
% endif
|
% endif
|
||||||
</div>
|
</div>
|
||||||
<div class="ui-block-b">
|
<div class="ui-block-b">
|
||||||
${h.image(product_image_url, "product image")}
|
% if product_image_url:
|
||||||
|
${h.image(product_image_url, "product image")}
|
||||||
|
% endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
% if not batch.truck_dump:
|
% if populated_from_purchase:
|
||||||
<tr>
|
<tr>
|
||||||
<td>ordered</td>
|
<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>
|
</tr>
|
||||||
% endif
|
% endif
|
||||||
<tr>
|
<tr>
|
||||||
<td>received</td>
|
<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>
|
||||||
<tr>
|
<tr>
|
||||||
<td>damaged</td>
|
<td>damaged</td>
|
||||||
<td>${h.pretty_quantity(row.cases_damaged or 0)} / ${h.pretty_quantity(row.units_damaged or 0)}</td>
|
<td>
|
||||||
</tr>
|
% if allow_cases:
|
||||||
<tr>
|
${h.pretty_quantity(row.cases_damaged or 0)} /
|
||||||
<td>expired</td>
|
% endif
|
||||||
<td>${h.pretty_quantity(row.cases_expired or 0)} / ${h.pretty_quantity(row.units_expired or 0)}</td>
|
${h.pretty_quantity(row.units_damaged or 0)}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
% if allow_expired:
|
||||||
|
<tr>
|
||||||
|
<td>expired</td>
|
||||||
|
<td>
|
||||||
|
% if allow_cases:
|
||||||
|
${h.pretty_quantity(row.cases_expired or 0)} /
|
||||||
|
% endif
|
||||||
|
${h.pretty_quantity(row.units_expired or 0)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
% endif
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
@ -59,9 +85,13 @@
|
||||||
${h.hidden('cases')}
|
${h.hidden('cases')}
|
||||||
${h.hidden('units')}
|
${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>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -70,7 +100,9 @@
|
||||||
<fieldset data-role="controlgroup" data-type="horizontal" class="receiving-mode">
|
<fieldset data-role="controlgroup" data-type="horizontal" class="receiving-mode">
|
||||||
${h.radio('mode', value='received', label="received", checked=True)}
|
${h.radio('mode', value='received', label="received", checked=True)}
|
||||||
${h.radio('mode', value='damaged', label="damaged")}
|
${h.radio('mode', value='damaged', label="damaged")}
|
||||||
${h.radio('mode', value='expired', label="expired")}
|
% if allow_expired:
|
||||||
|
${h.radio('mode', value='expired', label="expired")}
|
||||||
|
% endif
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -95,11 +127,13 @@
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
${h.hidden('quick_receive', value='false')}
|
${h.hidden('quick_receive', value='false')}
|
||||||
|
${h.end_form()}
|
||||||
|
|
||||||
${h.hidden('delete_row', value='false')}
|
% if master.mobile_rows_deletable and request.has_perm('{}.delete_row'.format(permission_prefix)):
|
||||||
% if 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')}
|
||||||
<button type="button" id="delete-receiving-row">Delete this Row</button>
|
${h.csrf_token(request)}
|
||||||
|
${h.submit('submit', "Delete this Row")}
|
||||||
|
${h.end_form()}
|
||||||
% endif
|
% endif
|
||||||
|
|
||||||
${h.end_form()}
|
|
||||||
% endif
|
% endif
|
||||||
|
|
|
@ -48,7 +48,6 @@ from rattail.progress import SocketProgress
|
||||||
|
|
||||||
import colander
|
import colander
|
||||||
import deform
|
import deform
|
||||||
from pyramid import httpexceptions
|
|
||||||
from pyramid.renderers import render_to_response
|
from pyramid.renderers import render_to_response
|
||||||
from pyramid.response import FileResponse
|
from pyramid.response import FileResponse
|
||||||
from webhelpers2.html import HTML, tags
|
from webhelpers2.html import HTML, tags
|
||||||
|
@ -1074,20 +1073,11 @@ class BatchMasterView(MasterView):
|
||||||
def get_parent(self, row):
|
def get_parent(self, row):
|
||||||
return row.batch
|
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
|
Perform the actual deletion of given row object.
|
||||||
row but does not truly delete it.
|
|
||||||
"""
|
"""
|
||||||
row = self.Session.query(self.model_row_class).get(self.request.matchdict['row_uuid'])
|
self.handler.remove_row(row)
|
||||||
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))
|
|
||||||
|
|
||||||
def bulk_delete_rows(self):
|
def bulk_delete_rows(self):
|
||||||
"""
|
"""
|
||||||
|
@ -1096,9 +1086,14 @@ class BatchMasterView(MasterView):
|
||||||
"""
|
"""
|
||||||
batch = self.get_instance()
|
batch = self.get_instance()
|
||||||
query = self.get_effective_row_data(sort=False)
|
query = self.get_effective_row_data(sort=False)
|
||||||
|
|
||||||
|
# TODO: this should surely be handled by the handler...
|
||||||
if batch.rowcount is not None:
|
if batch.rowcount is not None:
|
||||||
batch.rowcount -= query.count()
|
batch.rowcount -= query.count()
|
||||||
query.update({'removed': True}, synchronize_session=False)
|
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))
|
return self.redirect(self.get_action_url('view', batch))
|
||||||
|
|
||||||
def execute(self):
|
def execute(self):
|
||||||
|
|
|
@ -139,6 +139,7 @@ class MasterView(View):
|
||||||
mobile_rows_filterable = False
|
mobile_rows_filterable = False
|
||||||
mobile_rows_viewable = False
|
mobile_rows_viewable = False
|
||||||
mobile_rows_editable = False
|
mobile_rows_editable = False
|
||||||
|
mobile_rows_deletable = False
|
||||||
|
|
||||||
row_labels = {}
|
row_labels = {}
|
||||||
|
|
||||||
|
@ -2670,11 +2671,12 @@ class MasterView(View):
|
||||||
|
|
||||||
parent = self.get_parent(row)
|
parent = self.get_parent(row)
|
||||||
return self.render_to_response('edit_row', {
|
return self.render_to_response('edit_row', {
|
||||||
|
'row': row,
|
||||||
'instance': row,
|
'instance': row,
|
||||||
|
'parent_instance': parent,
|
||||||
'instance_title': self.get_row_instance_title(row),
|
'instance_title': self.get_row_instance_title(row),
|
||||||
'instance_url': instance_url,
|
'instance_url': instance_url,
|
||||||
'instance_deletable': self.row_deletable(row),
|
'instance_deletable': self.row_deletable(row),
|
||||||
'parent_instance': parent,
|
|
||||||
'parent_title': self.get_instance_title(parent),
|
'parent_title': self.get_instance_title(parent),
|
||||||
'parent_url': self.get_action_url('view', parent, mobile=True),
|
'parent_url': self.get_action_url('view', parent, mobile=True),
|
||||||
'form': form},
|
'form': form},
|
||||||
|
@ -2705,16 +2707,38 @@ class MasterView(View):
|
||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def delete_row_object(self, row):
|
||||||
|
"""
|
||||||
|
Perform the actual deletion of given row object.
|
||||||
|
"""
|
||||||
|
self.Session.delete(row)
|
||||||
|
|
||||||
def delete_row(self):
|
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'])
|
row = self.Session.query(self.model_row_class).get(self.request.matchdict['row_uuid'])
|
||||||
if not row:
|
if not row:
|
||||||
raise httpexceptions.HTTPNotFound()
|
raise self.notfound()
|
||||||
self.Session.delete(row)
|
self.delete_row_object(row)
|
||||||
return self.redirect(self.get_action_url('edit', self.get_parent(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):
|
def get_parent(self, row):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@ -3050,9 +3074,15 @@ class MasterView(View):
|
||||||
permission='{}.edit_row'.format(permission_prefix))
|
permission='{}.edit_row'.format(permission_prefix))
|
||||||
|
|
||||||
# delete row
|
# delete row
|
||||||
if cls.has_rows and cls.rows_deletable:
|
if cls.has_rows:
|
||||||
config.add_route('{}.delete_row'.format(route_prefix), '{}/{{uuid}}/rows/{{row_uuid}}/delete'.format(url_prefix))
|
if cls.rows_deletable or cls.mobile_rows_deletable:
|
||||||
config.add_view(cls, attr='delete_row', route_name='{}.delete_row'.format(route_prefix),
|
config.add_tailbone_permission(permission_prefix, '{}.delete_row'.format(permission_prefix),
|
||||||
permission='{}.delete_row'.format(permission_prefix))
|
"Delete individual {} rows".format(model_title))
|
||||||
config.add_tailbone_permission(permission_prefix, '{}.delete_row'.format(permission_prefix),
|
if cls.rows_deletable:
|
||||||
"Delete individual {} rows".format(model_title))
|
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 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')
|
default_active=True, default_verb='contains')
|
||||||
g.sorters['buyer'] = g.make_sorter(model.Person.display_name)
|
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_active = True
|
||||||
g.filters['date_ordered'].default_verb = 'equal'
|
g.filters['date_ordered'].default_verb = 'equal'
|
||||||
|
g.set_label('date_ordered', "Ordered")
|
||||||
g.set_sort_defaults('date_ordered', 'desc')
|
g.set_sort_defaults('date_ordered', 'desc')
|
||||||
|
|
||||||
g.set_enum('status', self.enum.PURCHASE_STATUS)
|
# date_received
|
||||||
|
g.filters['date_received'].default_active = True
|
||||||
g.set_label('date_ordered', "Ordered")
|
g.filters['date_received'].default_verb = 'equal'
|
||||||
g.set_label('date_received', "Received")
|
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.")
|
g.set_label('invoice_number', "Invoice No.")
|
||||||
|
|
||||||
def configure_form(self, f):
|
def configure_form(self, f):
|
||||||
|
|
|
@ -55,6 +55,7 @@ class OrderingBatchView(PurchasingBatchView):
|
||||||
mobile_rows_creatable = True
|
mobile_rows_creatable = True
|
||||||
mobile_rows_quickable = True
|
mobile_rows_quickable = True
|
||||||
mobile_rows_editable = True
|
mobile_rows_editable = True
|
||||||
|
mobile_rows_deletable = True
|
||||||
has_worksheet = True
|
has_worksheet = True
|
||||||
|
|
||||||
mobile_form_fields = [
|
mobile_form_fields = [
|
||||||
|
@ -186,7 +187,7 @@ class OrderingBatchView(PurchasingBatchView):
|
||||||
'history': history,
|
'history': history,
|
||||||
'get_upc': lambda p: p.upc.pretty() if p.upc else '',
|
'get_upc': lambda p: p.upc.pretty() if p.upc else '',
|
||||||
'header_columns': self.order_form_header_columns,
|
'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):
|
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
|
from __future__ import unicode_literals, absolute_import
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
import logging
|
||||||
|
|
||||||
import six
|
import six
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
|
|
||||||
from rattail import pod
|
from rattail import pod
|
||||||
from rattail.db import model, api
|
from rattail.db import model, api
|
||||||
|
from rattail.db.util import maxlen
|
||||||
from rattail.gpc import GPC
|
from rattail.gpc import GPC
|
||||||
from rattail.time import localtime
|
from rattail.time import localtime
|
||||||
from rattail.util import pretty_quantity, prettify, OrderedDict
|
from rattail.util import pretty_quantity, prettify, OrderedDict
|
||||||
|
@ -47,13 +49,16 @@ from tailbone import forms, grids
|
||||||
from tailbone.views.purchasing import PurchasingBatchView
|
from tailbone.views.purchasing import PurchasingBatchView
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class MobileItemStatusFilter(grids.filters.MobileFilter):
|
class MobileItemStatusFilter(grids.filters.MobileFilter):
|
||||||
|
|
||||||
value_choices = ['incomplete', 'unexpected', 'damaged', 'expired', 'all']
|
value_choices = ['incomplete', 'unexpected', 'damaged', 'expired', 'all']
|
||||||
|
|
||||||
def filter_equal(self, query, value):
|
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':
|
if value == 'received':
|
||||||
return query.filter(sa.or_(
|
return query.filter(sa.or_(
|
||||||
model.PurchaseBatchRow.cases_received != 0,
|
model.PurchaseBatchRow.cases_received != 0,
|
||||||
|
@ -105,6 +110,7 @@ class ReceivingBatchView(PurchasingBatchView):
|
||||||
mobile_rows_filterable = True
|
mobile_rows_filterable = True
|
||||||
mobile_rows_creatable = True
|
mobile_rows_creatable = True
|
||||||
mobile_rows_quickable = True
|
mobile_rows_quickable = True
|
||||||
|
mobile_rows_deletable = True
|
||||||
|
|
||||||
allow_from_po = False
|
allow_from_po = False
|
||||||
allow_from_scratch = True
|
allow_from_scratch = True
|
||||||
|
@ -354,13 +360,14 @@ class ReceivingBatchView(PurchasingBatchView):
|
||||||
kwargs = super(ReceivingBatchView, self).get_batch_kwargs(batch, mobile=mobile)
|
kwargs = super(ReceivingBatchView, self).get_batch_kwargs(batch, mobile=mobile)
|
||||||
|
|
||||||
if mobile:
|
if mobile:
|
||||||
purchase = self.get_purchase(self.request.POST['purchase'])
|
if 'purchase' in self.request.POST:
|
||||||
if isinstance(purchase, model.Purchase):
|
purchase = self.get_purchase(self.request.POST['purchase'])
|
||||||
kwargs['purchase'] = purchase
|
if isinstance(purchase, model.Purchase):
|
||||||
|
kwargs['purchase'] = purchase
|
||||||
|
|
||||||
department = self.department_for_purchase(purchase)
|
department = self.department_for_purchase(purchase)
|
||||||
if department:
|
if department:
|
||||||
kwargs['department'] = department
|
kwargs['department'] = department
|
||||||
|
|
||||||
else: # not mobile
|
else: # not mobile
|
||||||
batch_type = self.request.POST['batch_type']
|
batch_type = self.request.POST['batch_type']
|
||||||
|
@ -501,12 +508,19 @@ class ReceivingBatchView(PurchasingBatchView):
|
||||||
"""
|
"""
|
||||||
batch = self.get_instance()
|
batch = self.get_instance()
|
||||||
filters = grids.filters.GridFilterSet()
|
filters = grids.filters.GridFilterSet()
|
||||||
if batch.truck_dump:
|
|
||||||
value_choices = ['received', 'damaged', 'expired', 'all']
|
# visible filter options will depend on whether batch came from purchase
|
||||||
default_status = 'all'
|
if self.handler.populated_from_purchase(batch):
|
||||||
else:
|
|
||||||
value_choices = ['incomplete', 'unexpected', 'damaged', 'expired', 'all']
|
value_choices = ['incomplete', 'unexpected', 'damaged', 'expired', 'all']
|
||||||
default_status = 'incomplete'
|
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',
|
filters['status'] = MobileItemStatusFilter('status',
|
||||||
value_choices=value_choices,
|
value_choices=value_choices,
|
||||||
default_value=default_status)
|
default_value=default_status)
|
||||||
|
@ -522,10 +536,24 @@ class ReceivingBatchView(PurchasingBatchView):
|
||||||
mode = self.batch_mode
|
mode = self.batch_mode
|
||||||
data = {'mode': 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.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:
|
if not self.allow_truck_dump:
|
||||||
raise NotImplementedError("Requested workflow not supported: truck_dump")
|
raise NotImplementedError("Requested workflow not supported: truck_dump")
|
||||||
batch = self.model_class()
|
batch = self.model_class()
|
||||||
|
@ -611,22 +639,36 @@ class ReceivingBatchView(PurchasingBatchView):
|
||||||
def quick_locate_rows(self, batch, entry):
|
def quick_locate_rows(self, batch, entry):
|
||||||
rows = []
|
rows = []
|
||||||
|
|
||||||
# we prefer "exact" matches, i.e. those which assumed the entry already
|
# try to locate rows by product uuid match before other key
|
||||||
# contained the check digit.
|
product = self.Session.query(model.Product).get(entry)
|
||||||
provided = GPC(entry, calc_check_digit=False)
|
if product:
|
||||||
for row in batch.active_rows():
|
rows = [row for row in batch.active_rows()
|
||||||
if row.upc == provided:
|
if row.product_uuid == product.uuid]
|
||||||
rows.append(row)
|
if rows:
|
||||||
if rows:
|
return rows
|
||||||
|
|
||||||
|
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')
|
||||||
|
rows = [row for row in batch.active_rows()
|
||||||
|
if row.upc == checked]
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
# if no "exact" matches, we'll settle for those which assume the entry
|
elif key == 'item_id':
|
||||||
# lacked a check digit.
|
rows = [row for row in batch.active_rows()
|
||||||
checked = GPC(entry, calc_check_digit='upc')
|
if row.item_id == entry]
|
||||||
for row in batch.active_rows():
|
return rows
|
||||||
if row.upc == checked:
|
|
||||||
rows.append(row)
|
|
||||||
return rows
|
|
||||||
|
|
||||||
def save_quick_row_form(self, form):
|
def save_quick_row_form(self, form):
|
||||||
batch = self.get_instance()
|
batch = self.get_instance()
|
||||||
|
@ -652,30 +694,69 @@ class ReceivingBatchView(PurchasingBatchView):
|
||||||
self.Session.flush()
|
self.Session.flush()
|
||||||
return row
|
return row
|
||||||
|
|
||||||
# try to locate product by upc
|
# try to locate product by uuid before other, more specific key
|
||||||
provided = GPC(entry, calc_check_digit=False)
|
product = self.Session.query(model.Product).get(entry)
|
||||||
checked = GPC(entry, calc_check_digit='upc')
|
if product and not product.deleted:
|
||||||
product = api.get_product_by_upc(self.Session(), provided)
|
|
||||||
if not product:
|
|
||||||
product = api.get_product_by_upc(self.Session(), checked)
|
|
||||||
if product:
|
|
||||||
row = model.PurchaseBatchRow()
|
row = model.PurchaseBatchRow()
|
||||||
row.product = product
|
row.product = product
|
||||||
self.handler.add_row(batch, row)
|
self.handler.add_row(batch, row)
|
||||||
self.Session.flush()
|
self.Session.flush()
|
||||||
return row
|
return row
|
||||||
|
|
||||||
# check for "bad" upc
|
key = self.rattail_config.product_key()
|
||||||
if len(entry) > 14:
|
if key == 'upc':
|
||||||
return
|
|
||||||
|
|
||||||
# product not in system, but presumably sane upc, so add to batch anyway
|
# try to locate product by upc
|
||||||
row = model.PurchaseBatchRow()
|
provided = GPC(entry, calc_check_digit=False)
|
||||||
row.upc = provided # TODO: why not checked? how to know?
|
checked = GPC(entry, calc_check_digit='upc')
|
||||||
row.description = "(unknown product)"
|
product = api.get_product_by_upc(self.Session(), provided)
|
||||||
self.handler.add_row(batch, row)
|
if not product:
|
||||||
self.Session.flush()
|
product = api.get_product_by_upc(self.Session(), checked)
|
||||||
return row
|
if product:
|
||||||
|
row = model.PurchaseBatchRow()
|
||||||
|
row.product = product
|
||||||
|
self.handler.add_row(batch, row)
|
||||||
|
self.Session.flush()
|
||||||
|
return row
|
||||||
|
|
||||||
|
# check for "bad" upc
|
||||||
|
if len(entry) > 14:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 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):
|
def redirect_after_quick_row(self, row, mobile=False):
|
||||||
if mobile:
|
if mobile:
|
||||||
|
@ -695,11 +776,15 @@ class ReceivingBatchView(PurchasingBatchView):
|
||||||
context = {
|
context = {
|
||||||
'row': row,
|
'row': row,
|
||||||
'batch': batch,
|
'batch': batch,
|
||||||
|
'parent_instance': batch,
|
||||||
'instance': row,
|
'instance': row,
|
||||||
'instance_title': self.get_row_instance_title(row),
|
'instance_title': self.get_row_instance_title(row),
|
||||||
'parent_model_title': self.get_model_title(),
|
'parent_model_title': self.get_model_title(),
|
||||||
'product_image_url': pod.get_image_url(self.rattail_config, row.upc),
|
'product_image_url': pod.get_image_url(self.rattail_config, row.upc),
|
||||||
'form': form,
|
'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)):
|
if self.request.has_perm('{}.create_row'.format(permission_prefix)):
|
||||||
|
@ -707,56 +792,47 @@ class ReceivingBatchView(PurchasingBatchView):
|
||||||
update_form = forms.Form(schema=schema, request=self.request)
|
update_form = forms.Form(schema=schema, request=self.request)
|
||||||
if update_form.validate(newstyle=True):
|
if update_form.validate(newstyle=True):
|
||||||
row = self.Session.query(model.PurchaseBatchRow).get(update_form.validated['row'])
|
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']
|
||||||
|
|
||||||
# TODO: surely this (delete_row) should be split out to a separate view
|
# add values as-is to existing case/unit amounts. note
|
||||||
if update_form.validated['delete_row']:
|
# that this can sometimes give us negative values! e.g. if
|
||||||
if not self.request.has_perm('{}.delete_row'.format(permission_prefix)):
|
# user scans 1 CS and then subtracts 2 EA, then we would
|
||||||
raise httpexceptions.HTTPForbidden()
|
# have 1 / -2 for our counts. but we consider that to be
|
||||||
self.handler.remove_row(row)
|
# expected, and other logic must allow for the possibility
|
||||||
return self.redirect(self.get_action_url('view', batch, mobile=True))
|
if cases:
|
||||||
|
setattr(row, 'cases_{}'.format(mode),
|
||||||
|
(getattr(row, 'cases_{}'.format(mode)) or 0) + cases)
|
||||||
|
if units:
|
||||||
|
setattr(row, 'units_{}'.format(mode),
|
||||||
|
(getattr(row, 'units_{}'.format(mode)) or 0) + units)
|
||||||
|
|
||||||
else: # not delete_row
|
# if mode in ('damaged', 'expired', 'mispick'):
|
||||||
mode = update_form.validated['mode']
|
if mode in ('damaged', 'expired'):
|
||||||
cases = update_form.validated['cases']
|
self.attach_credit(row, mode, cases, units,
|
||||||
units = update_form.validated['units']
|
expiration_date=update_form.validated['expiration_date'],
|
||||||
|
# discarded=update_form.data['trash'],
|
||||||
|
# mispick_product=shipped_product)
|
||||||
|
)
|
||||||
|
|
||||||
# add values as-is to existing case/unit amounts. note
|
# first undo any totals previously in effect for the row, then refresh
|
||||||
# that this can sometimes give us negative values! e.g. if
|
if row.invoice_total:
|
||||||
# user scans 1 CS and then subtracts 2 EA, then we would
|
batch.invoice_total -= row.invoice_total
|
||||||
# have 1 / -2 for our counts. but we consider that to be
|
self.handler.refresh_row(row)
|
||||||
# expected, and other logic must allow for the possibility
|
|
||||||
if cases:
|
|
||||||
setattr(row, 'cases_{}'.format(mode),
|
|
||||||
(getattr(row, 'cases_{}'.format(mode)) or 0) + cases)
|
|
||||||
if units:
|
|
||||||
setattr(row, 'units_{}'.format(mode),
|
|
||||||
(getattr(row, 'units_{}'.format(mode)) or 0) + units)
|
|
||||||
|
|
||||||
# if mode in ('damaged', 'expired', 'mispick'):
|
# keep track of last-used uom, although we just track
|
||||||
if mode in ('damaged', 'expired'):
|
# whether or not it was 'CS' since the unit_uom can vary
|
||||||
self.attach_credit(row, mode, cases, units,
|
sticky_case = None
|
||||||
expiration_date=update_form.validated['expiration_date'],
|
if not update_form.validated['quick_receive']:
|
||||||
# discarded=update_form.data['trash'],
|
if cases and not units:
|
||||||
# mispick_product=shipped_product)
|
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
|
||||||
|
|
||||||
# first undo any totals previously in effect for the row, then refresh
|
return self.redirect(self.get_action_url('view', batch, mobile=True))
|
||||||
if row.invoice_total:
|
|
||||||
batch.invoice_total -= row.invoice_total
|
|
||||||
self.handler.refresh_row(row)
|
|
||||||
|
|
||||||
# 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
|
# unit_uom can vary by product
|
||||||
context['unit_uom'] = 'LB' if row.product and row.product.weighed else 'EA'
|
context['unit_uom'] = 'LB' if row.product and row.product.weighed else 'EA'
|
||||||
|
@ -773,7 +849,7 @@ class ReceivingBatchView(PurchasingBatchView):
|
||||||
if context['uom'] == 'CS' and row.units_ordered and not row.cases_ordered:
|
if context['uom'] == 'CS' and row.units_ordered and not row.cases_ordered:
|
||||||
context['uom'] = context['unit_uom']
|
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')
|
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)
|
return self.render_to_response('view_row', context, mobile=True)
|
||||||
|
|
||||||
|
@ -844,9 +920,24 @@ class ReceivingBatchView(PurchasingBatchView):
|
||||||
cls._defaults(config)
|
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):
|
class MobileNewReceivingBatch(colander.MappingSchema):
|
||||||
|
|
||||||
vendor = colander.SchemaNode(forms.types.VendorType())
|
vendor = colander.SchemaNode(colander.String(),
|
||||||
|
validator=valid_vendor)
|
||||||
|
|
||||||
workflow = colander.SchemaNode(colander.String(),
|
workflow = colander.SchemaNode(colander.String(),
|
||||||
validator=colander.OneOf([
|
validator=colander.OneOf([
|
||||||
|
@ -895,8 +986,6 @@ class MobileReceivingForm(colander.MappingSchema):
|
||||||
|
|
||||||
quick_receive = colander.SchemaNode(colander.Boolean())
|
quick_receive = colander.SchemaNode(colander.Boolean())
|
||||||
|
|
||||||
delete_row = colander.SchemaNode(colander.Boolean())
|
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
ReceivingBatchView.defaults(config)
|
ReceivingBatchView.defaults(config)
|
||||||
|
|
|
@ -40,6 +40,8 @@ class SettingsView(MasterView):
|
||||||
Master view for the settings model.
|
Master view for the settings model.
|
||||||
"""
|
"""
|
||||||
model_class = model.Setting
|
model_class = model.Setting
|
||||||
|
model_title = "Raw Setting"
|
||||||
|
model_title_plural = "Raw Settings"
|
||||||
feedback = re.compile(r'^rattail\.mail\.user_feedback\..*')
|
feedback = re.compile(r'^rattail\.mail\.user_feedback\..*')
|
||||||
|
|
||||||
grid_columns = [
|
grid_columns = [
|
||||||
|
|
Loading…
Reference in a new issue