${h.pretty_quantity(row.cases_ordered or 0)} / ${h.pretty_quantity(row.units_ordered or 0)}
-
+ % if not batch.truck_dump:
+
+
ordered
+
${h.pretty_quantity(row.cases_ordered or 0)} / ${h.pretty_quantity(row.units_ordered or 0)}
+
+ % endif
received
${h.pretty_quantity(row.cases_received or 0)} / ${h.pretty_quantity(row.units_received or 0)}
@@ -57,7 +59,7 @@
% endfor
% endif
-% if not instance.batch.executed and not instance.batch.complete:
+% if not batch.executed and not batch.complete:
${h.form(request.current_route_url(), class_='receiving-update')}
${h.csrf_token(request)}
@@ -98,5 +100,10 @@
+ ${h.hidden('delete_row', value='false')}
+ % if request.has_perm('{}.delete_row'.format(permission_prefix)):
+
+ % endif
+
${h.end_form()}
% endif
diff --git a/tailbone/views/purchasing/batch.py b/tailbone/views/purchasing/batch.py
index 0f172928..fc5d1e37 100644
--- a/tailbone/views/purchasing/batch.py
+++ b/tailbone/views/purchasing/batch.py
@@ -512,6 +512,7 @@ class PurchasingBatchView(BatchMasterView):
def get_batch_kwargs(self, batch, mobile=False):
kwargs = super(PurchasingBatchView, self).get_batch_kwargs(batch, mobile=mobile)
kwargs['mode'] = self.batch_mode
+ kwargs['truck_dump'] = batch.truck_dump
if batch.store:
kwargs['store'] = batch.store
elif batch.store_uuid:
diff --git a/tailbone/views/purchasing/receiving.py b/tailbone/views/purchasing/receiving.py
index 6d5038ec..039b32ed 100644
--- a/tailbone/views/purchasing/receiving.py
+++ b/tailbone/views/purchasing/receiving.py
@@ -36,6 +36,8 @@ from rattail.gpc import GPC
from rattail.util import pretty_quantity, prettify
import colander
+from deform import widget as dfwidget
+from pyramid import httpexceptions
from webhelpers2.html import tags
from tailbone import forms, grids
@@ -48,6 +50,12 @@ class MobileItemStatusFilter(grids.filters.MobileFilter):
def filter_equal(self, query, value):
+ # NOTE: this is only relevant for truck dump
+ if value == 'received':
+ return query.filter(sa.or_(
+ model.PurchaseBatchRow.cases_received != 0,
+ model.PurchaseBatchRow.units_received != 0))
+
# TODO: is this accurate (enough) ?
if value == 'incomplete':
return query.filter(sa.or_(model.PurchaseBatchRow.cases_ordered != 0, model.PurchaseBatchRow.units_ordered != 0))\
@@ -95,10 +103,29 @@ class ReceivingBatchView(PurchasingBatchView):
mobile_rows_filterable = True
mobile_rows_creatable = True
+ allow_from_po = False
+ allow_from_scratch = True
+ allow_truck_dump = False
+
+ grid_columns = [
+ 'id',
+ 'vendor',
+ 'truck_dump',
+ 'department',
+ 'buyer',
+ 'date_ordered',
+ 'created',
+ 'created_by',
+ 'rowcount',
+ 'status_code',
+ 'executed',
+ ]
+
form_fields = [
'id',
'store',
'vendor',
+ 'truck_dump',
'department',
'purchase',
'vendor_email',
@@ -123,6 +150,7 @@ class ReceivingBatchView(PurchasingBatchView):
mobile_form_fields = [
'vendor',
+ 'truck_dump',
'department',
]
@@ -175,6 +203,13 @@ class ReceivingBatchView(PurchasingBatchView):
def batch_mode(self):
return self.enum.PURCHASE_BATCH_MODE_RECEIVING
+ def configure_form(self, f):
+ super(ReceivingBatchView, self).configure_form(f)
+
+ # truck_dump
+ if self.editing:
+ f.set_readonly('truck_dump')
+
def render_mobile_listitem(self, batch, i):
title = "({}) {} for ${:0,.2f} - {}, {}".format(
batch.id_str,
@@ -188,8 +223,17 @@ class ReceivingBatchView(PurchasingBatchView):
"""
Returns a set of filters for the mobile row grid.
"""
+ batch = self.get_instance()
filters = grids.filters.GridFilterSet()
- filters['status'] = MobileItemStatusFilter('status', default_value='incomplete')
+ if batch.truck_dump:
+ value_choices = ['received', 'damaged', 'expired', 'all']
+ default_status = 'all'
+ else:
+ value_choices = ['incomplete', 'unexpected', 'damaged', 'expired', 'all']
+ default_status = 'incomplete'
+ filters['status'] = MobileItemStatusFilter('status',
+ value_choices=value_choices,
+ default_value=default_status)
return filters
def mobile_create(self):
@@ -199,6 +243,25 @@ class ReceivingBatchView(PurchasingBatchView):
mode = self.batch_mode
data = {'mode': mode}
+ form = forms.Form(schema=MobileNewReceivingBatch(), request=self.request)
+ if form.validate(newstyle=True):
+
+ if 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.merge(form.validated['vendor'])
+ batch.created_by = self.request.user
+ 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))
+
+ else:
+ raise NotImplementedError("Requested workflow not supported: {}".format(form.validated['workflow']))
+
vendor = None
if self.request.method == 'POST' and self.request.POST.get('vendor'):
vendor = self.Session.query(model.Vendor).get(self.request.POST['vendor'])
@@ -227,28 +290,19 @@ class ReceivingBatchView(PurchasingBatchView):
data['purchases'] = [(p['key'], p['display']) for p in purchases['purchases']]
return self.render_to_response('create', data, mobile=True)
- def get_batch_kwargs(self, batch, mobile=False):
- kwargs = super(ReceivingBatchView, self).get_batch_kwargs(batch, mobile=mobile)
- if mobile:
-
- purchase = self.get_purchase(self.request.POST['purchase'])
- numbers = [d.F03 for d in purchase.details]
- if numbers:
- number = max(set(numbers), key=numbers.count)
- kwargs['department'] = self.Session.query(model.Department)\
- .filter(model.Department.number == number)\
- .one()
-
- return kwargs
-
def configure_mobile_form(self, f):
super(ReceivingBatchView, self).configure_mobile_form(f)
+ batch = f.model_instance
- # vendor
- # fs.vendor.with_renderer(fa.TextFieldRenderer),
+ # truck_dump
+ if not self.creating:
+ if not batch.truck_dump:
+ f.remove_field('truck_dump')
# department
- # fs.department.with_renderer(fa.TextFieldRenderer),
+ if not self.creating:
+ if batch.truck_dump:
+ f.remove_field('department')
def configure_row_form(self, f):
super(ReceivingBatchView, self).configure_row_form(f)
@@ -302,8 +356,7 @@ class ReceivingBatchView(PurchasingBatchView):
if product:
row = model.PurchaseBatchRow()
row.product = product
- batch.add_row(row)
- self.handler.refresh_row(row)
+ self.handler.add_row(batch, row)
# check for "bad" upc
elif len(upc) > 14:
@@ -329,9 +382,12 @@ class ReceivingBatchView(PurchasingBatchView):
"""
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,
'instance': row,
'instance_title': self.get_row_instance_title(row),
'parent_model_title': self.get_model_title(),
@@ -339,36 +395,45 @@ class ReceivingBatchView(PurchasingBatchView):
'form': form,
}
- if self.request.has_perm('{}.create_row'.format(self.get_permission_prefix())):
- update_form = forms.Form(schema=ReceivingForm(), request=self.request)
+ if self.request.has_perm('{}.create_row'.format(permission_prefix)):
+ update_form = forms.Form(schema=MobileReceivingForm(), request=self.request)
if update_form.validate(newstyle=True):
row = self.Session.merge(update_form.validated['row'])
- mode = update_form.validated['mode']
- cases = update_form.validated['cases']
- units = update_form.validated['units']
- if cases:
- setattr(row, 'cases_{}'.format(mode),
- (getattr(row, 'cases_{}'.format(mode)) or 0) + cases)
- if units:
- setattr(row, 'units_{}'.format(mode),
- (getattr(row, 'units_{}'.format(mode)) or 0) + units)
- # if mode in ('damaged', 'expired', 'mispick'):
- if mode in ('damaged', 'expired'):
- self.attach_credit(row, mode, cases, units,
- expiration_date=update_form.validated['expiration_date'],
- # discarded=update_form.data['trash'],
- # mispick_product=shipped_product)
- )
+ # 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))
- # first undo any totals previously in effect for the row, then refresh
- if row.invoice_total:
- row.batch.invoice_total -= row.invoice_total
- self.handler.refresh_row(row)
+ else: # not delete_row
+ mode = update_form.validated['mode']
+ cases = update_form.validated['cases']
+ units = update_form.validated['units']
+ if cases:
+ setattr(row, 'cases_{}'.format(mode),
+ (getattr(row, 'cases_{}'.format(mode)) or 0) + cases)
+ if units:
+ setattr(row, 'units_{}'.format(mode),
+ (getattr(row, 'units_{}'.format(mode)) or 0) + units)
- return self.redirect(self.request.route_url('mobile.{}.view'.format(self.get_route_prefix()), uuid=row.batch_uuid))
+ # if mode in ('damaged', 'expired', 'mispick'):
+ if mode in ('damaged', 'expired'):
+ self.attach_credit(row, mode, cases, units,
+ expiration_date=update_form.validated['expiration_date'],
+ # discarded=update_form.data['trash'],
+ # mispick_product=shipped_product)
+ )
- if not row.cases_ordered and not row.units_ordered:
+ # first undo any totals previously in effect for the row, then refresh
+ if row.invoice_total:
+ batch.invoice_total -= row.invoice_total
+ self.handler.refresh_row(row)
+
+ return self.redirect(self.get_action_url('view', batch, mobile=True))
+
+ if not row.cases_ordered and not row.units_ordered and not batch.truck_dump:
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)
@@ -438,22 +503,39 @@ class PurchaseBatchRowType(forms.types.ObjectType):
return row
-class ReceivingForm(colander.MappingSchema):
+class MobileNewReceivingBatch(colander.MappingSchema):
+
+ vendor = colander.SchemaNode(forms.types.VendorType())
+
+ workflow = colander.SchemaNode(colander.String(),
+ validator=colander.OneOf([
+ 'from_po',
+ 'from_scratch',
+ 'truck_dump',
+ ]))
+
+
+class MobileReceivingForm(colander.MappingSchema):
row = colander.SchemaNode(PurchaseBatchRowType())
mode = colander.SchemaNode(colander.String(),
- validator=colander.OneOf(['received',
- 'damaged',
- 'expired',
- # 'mispick',
+ validator=colander.OneOf([
+ 'received',
+ 'damaged',
+ 'expired',
+ # 'mispick',
]))
cases = colander.SchemaNode(colander.Decimal(), missing=None)
units = colander.SchemaNode(colander.Decimal(), missing=None)
- expiration_date = colander.SchemaNode(colander.Date(), missing=colander.null)
+ expiration_date = colander.SchemaNode(colander.Date(),
+ widget=dfwidget.TextInputWidget(),
+ missing=colander.null)
+
+ delete_row = colander.SchemaNode(colander.Boolean())
def includeme(config):