Add support for 'receiving' mode for purchase batches

This commit is contained in:
Lance Edgar 2016-11-21 01:07:35 -06:00
parent 8acb9b0029
commit 67f6c11307
9 changed files with 292 additions and 70 deletions

View file

@ -49,6 +49,12 @@ class ProductFieldRenderer(AutocompleteFieldRenderer):
return product.full_description
return ''
def render_readonly(self, **kwargs):
product = self.raw_value
if not product:
return ''
return tags.link_to(product, self.request.route_url('products.view', uuid=product.uuid))
class GPCFieldRenderer(TextFieldRenderer):
"""

View file

@ -26,7 +26,7 @@ Vendor Field Renderers
from __future__ import unicode_literals, absolute_import
import formalchemy as fa
from formalchemy.fields import SelectFieldRenderer
from webhelpers.html import tags
from tailbone.forms.renderers.common import AutocompleteFieldRenderer
@ -45,7 +45,7 @@ class VendorFieldRenderer(AutocompleteFieldRenderer):
return tags.link_to(vendor, self.request.route_url('vendors.view', uuid=vendor.uuid))
class PurchaseFieldRenderer(fa.FieldRenderer):
class PurchaseFieldRenderer(SelectFieldRenderer):
"""
Renderer for :class:`rattail.db.model.Purchase` relation fields.
"""

View file

@ -27,7 +27,6 @@ Event Subscribers
from __future__ import unicode_literals, absolute_import
import rattail
from rattail import enum
from rattail.db import model
from rattail.db.auth import has_permission, administrator_role
@ -79,6 +78,7 @@ def before_render(event):
renderer_globals['url'] = request.route_url
renderer_globals['rattail'] = rattail
renderer_globals['tailbone'] = tailbone
renderer_globals['enum'] = request.rattail_config.get_enum()
def add_inbox_count(event):
@ -92,6 +92,7 @@ def add_inbox_count(event):
request = event.get('request') or threadlocal.get_current_request()
if request.user:
renderer_globals = event
enum = request.rattail_config.get_enum()
renderer_globals['inbox_count'] = Session.query(model.Message)\
.outerjoin(model.MessageRecipient)\
.filter(model.MessageRecipient.recipient == Session.merge(request.user))\

View file

@ -4,11 +4,71 @@
<%def name="head_tags()">
${parent.head_tags()}
<script type="text/javascript">
function show_mode(mode) {
if (mode == ${enum.PURCHASE_BATCH_MODE_NEW}) {
$('.field-wrapper.store_uuid').show();
$('.field-wrapper.purchase_uuid').hide();
$('.field-wrapper.buyer_uuid').show();
$('.field-wrapper.date_ordered').show();
$('.field-wrapper.date_received').hide();
} else if (mode == ${enum.PURCHASE_BATCH_MODE_RECEIVING}) {
$('.field-wrapper.store_uuid').hide();
$('.field-wrapper.purchase_uuid').show();
$('.field-wrapper.buyer_uuid').hide();
$('.field-wrapper.date_ordered').hide();
$('.field-wrapper.date_received').show();
}
}
function vendor_selected(uuid, name) {
var mode = $('.mode select').val();
if (mode == ${enum.PURCHASE_BATCH_MODE_RECEIVING}) {
var purchases = $('.purchase_uuid select');
purchases.empty();
var data = {'vendor_uuid': uuid, 'mode': mode};
$.get('${url('purchases.batch.eligible_purchases')}', data, function(data) {
if (data.error) {
alert(data.error);
} else {
$.each(data.purchases, function(i, purchase) {
purchases.append($('<option value="' + purchase.uuid + '">' + purchase.display + '</option>'));
});
}
});
// TODO: apparently refresh doesn't work right?
// http://stackoverflow.com/a/10280078
// purchases.selectmenu('refresh');
purchases.selectmenu('destroy').selectmenu();
}
}
function vendor_cleared() {
var purchases = $('.purchase_uuid select');
purchases.empty();
// TODO: apparently refresh doesn't work right?
// http://stackoverflow.com/a/10280078
// purchases.selectmenu('refresh');
purchases.selectmenu('destroy').selectmenu();
}
$(function() {
$('.field-wrapper.mode select').selectmenu();
$('.field-wrapper.mode select').selectmenu({
change: function(event, ui) {
show_mode(ui.item.value);
}
});
$('.field-wrapper.purchase_uuid select').selectmenu();
show_mode(${form.fieldset.model.mode or enum.PURCHASE_BATCH_MODE_NEW});
});
</script>
</%def>

View file

@ -154,7 +154,18 @@
<th>Pref.</th>
<th>Unit Cost</th>
% for data in history.itervalues():
<th>${data['purchase'].date_ordered.strftime('%m/%d') if data else ''}</th>
<th>
% if data:
% if data['purchase'].date_ordered:
${data['purchase'].date_ordered.strftime('%m/%d') if data else ''}
% elif data['purchase'].date_received:
Rec.<br />
${data['purchase'].date_received.strftime('%m/%d') if data else ''}
% else:
??
% endif
% endif
</th>
% endfor
<th>
${batch.date_ordered.strftime('%m/%d')}<br />
@ -180,9 +191,15 @@
<td class="unit-cost">$${'{:0.2f}'.format(cost.unit_cost)}</td>
% for data in history.itervalues():
<td class="scratch_pad">
% if data and cost.product_uuid in data['items']:
${'{} / {}'.format(int(data['items'][cost.product_uuid].cases_ordered or 0), int(data['items'][cost.product_uuid].units_ordered or 0))}
## ${int(data['items'][cost.product_uuid].cases_ordered or 0) or ''}
% if data:
<% item = data['items'].get(cost.product_uuid) %>
% if item:
% if data['purchase'].date_ordered and (item.cases_ordered is not None or item.units_ordered is not None):
${'{} / {}'.format(int(item.cases_ordered or 0), int(item.units_ordered or 0))}
% elif item.cases_received is not None or item.units_received is not None:
${'{} / {}'.format(int(item.cases_received or 0), int(item.units_received or 0))}
% endif
% endif
% endif
</td>
% endfor

View file

@ -23,7 +23,7 @@
</%def>
<%def name="leading_buttons()">
% if not batch.complete and not batch.executed and request.has_perm('purchases.batch.order_form'):
% if batch.mode == enum.PURCHASE_BATCH_MODE_NEW and not batch.complete and not batch.executed and request.has_perm('purchases.batch.order_form'):
<button type="button" id="order-form">View as Order Form</button>
% endif
</%def>

View file

@ -237,19 +237,18 @@ class BatchMasterView(MasterView):
def save_create_form(self, form):
self.before_create(form)
# current user is batch creator
creator = self.request.user or self.late_login_user()
# transfer form data to batch instance
form.fieldset.sync()
batch = form.fieldset.model
batch.created_by = creator
# destroy initial batch and re-make using handler
kwargs = self.get_batch_kwargs(batch)
Session.expunge(batch)
# TODO: is no_autoflush necessary?
with Session.no_autoflush:
# transfer form data to batch instance
form.fieldset.sync()
batch = form.fieldset.model
# current user is batch creator
batch.created_by = self.request.user or self.late_login_user()
# destroy initial batch and re-make using handler
kwargs = self.get_batch_kwargs(batch)
Session.expunge(batch)
batch = self.handler.make_batch(Session(), **kwargs)
Session.flush()
@ -279,6 +278,7 @@ class BatchMasterView(MasterView):
kwargs['filename'] = batch.filename
return kwargs
# TODO: deprecate / remove this (is it used at all now?)
def init_batch(self, batch):
"""
Initialize a new batch. Derived classes can override this to
@ -667,7 +667,7 @@ class BatchMasterView(MasterView):
fs.status_text.set(readonly=True)
fs.removed.set(readonly=True)
try:
fs.product.set(readonly=True)
fs.product.set(readonly=True, renderer=forms.renderers.ProductFieldRenderer)
except AttributeError:
pass

View file

@ -36,6 +36,7 @@ from rattail.core import Object
from rattail.util import OrderedDict
import formalchemy as fa
from pyramid import httpexceptions
from tailbone import forms
from tailbone.db import Session
@ -56,6 +57,9 @@ class PurchaseBatchView(BatchMasterView):
rows_editable = True
edit_with_rows = False
def get_instance_title(self, batch):
return '{} ({})'.format(batch.id_str, self.enum.PURCHASE_BATCH_MODE[batch.mode])
def _preconfigure_grid(self, g):
super(PurchaseBatchView, self)._preconfigure_grid(g)
@ -74,6 +78,7 @@ class PurchaseBatchView(BatchMasterView):
g.filters['complete'].default_verb = 'is_true'
g.date_ordered.set(label="Ordered")
g.date_received.set(label="Received")
g.mode.set(renderer=forms.renderers.EnumFieldRenderer(self.enum.PURCHASE_BATCH_MODE))
def configure_grid(self, g):
@ -93,11 +98,14 @@ class PurchaseBatchView(BatchMasterView):
def _preconfigure_fieldset(self, fs):
super(PurchaseBatchView, self)._preconfigure_fieldset(fs)
fs.mode.set(renderer=forms.renderers.EnumFieldRenderer(self.enum.PURCHASE_BATCH_MODE))
fs.purchase.set(renderer=forms.renderers.PurchaseFieldRenderer)
fs.vendor.set(renderer=forms.renderers.VendorFieldRenderer)
fs.purchase.set(renderer=forms.renderers.PurchaseFieldRenderer, options=[])
fs.vendor.set(renderer=forms.renderers.VendorFieldRenderer,
attrs={'selected': 'vendor_selected',
'cleared': 'vendor_cleared'})
fs.buyer.set(renderer=forms.renderers.EmployeeFieldRenderer)
fs.po_number.set(label="PO Number")
fs.po_total.set(label="PO Total", readonly=True)
fs.po_total.set(label="PO Total", readonly=True, renderer=forms.renderers.CurrencyFieldRenderer)
fs.invoice_total.set(readonly=True, renderer=forms.renderers.CurrencyFieldRenderer)
fs.append(fa.Field('vendor_email', readonly=True,
value=lambda b: b.vendor.email.address if b.vendor.email else None))
@ -123,17 +131,19 @@ class PurchaseBatchView(BatchMasterView):
include=[
fs.id,
fs.mode,
fs.purchase,
fs.store,
fs.vendor,
fs.purchase,
fs.vendor_email,
fs.vendor_fax,
fs.vendor_contact,
fs.vendor_phone,
fs.buyer,
fs.date_ordered,
fs.date_received,
fs.po_number,
fs.po_total,
fs.invoice_total,
fs.created,
fs.created_by,
fs.complete,
@ -142,8 +152,8 @@ class PurchaseBatchView(BatchMasterView):
])
if self.creating:
del fs.purchase
del fs.po_total
del fs.invoice_total
del fs.complete
del fs.vendor_email
del fs.vendor_fax
@ -163,12 +173,14 @@ class PurchaseBatchView(BatchMasterView):
if buyer:
fs.model.buyer = buyer
# default order date is today
fs.model.date_ordered = localtime(self.rattail_config).date()
# TODO: something tells me this isn't quite safe..
# all dates have today as default
today = localtime(self.rattail_config).date()
fs.model.date_ordered = today
fs.model.date_received = today
# TODO: temp hack until we support more modes
modes = dict(self.enum.PURCHASE_BATCH_MODE)
del modes[self.enum.PURCHASE_BATCH_MODE_RECEIVING]
del modes[self.enum.PURCHASE_BATCH_MODE_COSTING]
fs.mode.set(renderer=forms.renderers.EnumFieldRenderer(modes))
@ -176,6 +188,30 @@ class PurchaseBatchView(BatchMasterView):
fs.mode.set(readonly=True)
fs.store.set(readonly=True)
fs.vendor.set(readonly=True)
fs.purchase.set(readonly=True)
def eligible_purchases(self):
uuid = self.request.GET.get('vendor_uuid')
vendor = Session.query(model.Vendor).get(uuid) if uuid else None
if not vendor:
return {'error': "Must specify a vendor."}
mode = self.request.GET.get('mode')
mode = int(mode) if mode and mode.isdigit() else None
if not mode or mode not in self.enum.PURCHASE_BATCH_MODE:
return {'error': "Unknown mode: {}".format(mode)}
purchases = Session.query(model.Purchase)\
.filter(model.Purchase.vendor == vendor)
if mode == enum.PURCHASE_BATCH_MODE_RECEIVING:
purchases = purchases.filter(model.Purchase.status == self.enum.PURCHASE_STATUS_ORDERED)\
.order_by(model.Purchase.date_ordered, model.Purchase.created)
return {'purchases': [{'uuid': p.uuid, 'display': self.render_eligible_purchase(p)}
for p in purchases]}
def render_eligible_purchase(self, purchase):
return '{} for ${:0,.2f} ({})'.format(purchase.date_ordered, purchase.po_total, purchase.buyer)
def get_batch_kwargs(self, batch):
kwargs = super(PurchaseBatchView, self).get_batch_kwargs(batch)
@ -192,8 +228,20 @@ class PurchaseBatchView(BatchMasterView):
kwargs['buyer'] = batch.buyer
elif batch.buyer_uuid:
kwargs['buyer_uuid'] = batch.buyer_uuid
kwargs['date_ordered'] = batch.date_ordered
kwargs['po_number'] = batch.po_number
if batch.mode == self.enum.PURCHASE_BATCH_MODE_NEW:
kwargs['date_ordered'] = batch.date_ordered
elif batch.mode == self.enum.PURCHASE_BATCH_MODE_RECEIVING:
kwargs['date_received'] = batch.date_received
if batch.purchase_uuid:
purchase = Session.query(model.Purchase).get(batch.purchase_uuid)
assert purchase
kwargs['purchase'] = purchase
kwargs['date_ordered'] = purchase.date_ordered
kwargs['po_total'] = purchase.po_total
return kwargs
def template_kwargs_view(self, **kwargs):
@ -214,11 +262,16 @@ class PurchaseBatchView(BatchMasterView):
g.upc.set(label="UPC")
g.brand_name.set(label="Brand")
g.cases_ordered.set(label="Cases")
g.units_ordered.set(label="Units")
g.po_total.set(label="Total")
g.cases_ordered.set(label="Cases Ord.")
g.units_ordered.set(label="Units Ord.")
g.cases_received.set(label="Cases Rec.")
g.units_received.set(label="Units Rec.")
g.po_total.set(label="Total", renderer=forms.renderers.CurrencyFieldRenderer)
g.invoice_total.set(label="Total", renderer=forms.renderers.CurrencyFieldRenderer)
def configure_row_grid(self, g):
batch = self.get_instance()
g.configure(
include=[
g.sequence,
@ -228,32 +281,38 @@ class PurchaseBatchView(BatchMasterView):
g.size,
g.cases_ordered,
g.units_ordered,
g.cases_received,
g.units_received,
g.po_total,
g.invoice_total,
g.status_code,
],
readonly=True)
if batch.mode == self.enum.PURCHASE_BATCH_MODE_NEW:
del g.cases_received
del g.units_received
del g.invoice_total
elif batch.mode == self.enum.PURCHASE_BATCH_MODE_RECEIVING:
del g.po_total
def make_row_grid_tools(self, batch):
return self.make_default_row_grid_tools(batch)
# def row_grid_row_attrs(self, row, i):
# attrs = {}
# if row.status_code in (row.STATUS_NOT_IN_PURCHASE,
# row.STATUS_NOT_IN_INVOICE,
# row.STATUS_DIFFERS_FROM_PURCHASE):
# attrs['class_'] = 'notice'
# if row.status_code in (row.STATUS_NOT_IN_DB,
# row.STATUS_COST_NOT_IN_DB,
# row.STATUS_NO_CASE_QUANTITY):
# attrs['class_'] = 'warning'
# return attrs
def row_grid_row_attrs(self, row, i):
attrs = {}
if row.status_code in (row.STATUS_INCOMPLETE,
row.STATUS_ORDERED_RECEIVED_DIFFER):
attrs['class_'] = 'notice'
return attrs
def _preconfigure_row_fieldset(self, fs):
super(PurchaseBatchView, self)._preconfigure_row_fieldset(fs)
fs.upc.set(label="UPC")
fs.brand_name.set(label="Brand")
fs.po_unit_cost.set(label="PO Unit Cost")
fs.po_total.set(label="PO Total")
fs.po_total.set(label="PO Total", renderer=forms.renderers.CurrencyFieldRenderer)
fs.invoice_total.set(renderer=forms.renderers.CurrencyFieldRenderer)
fs.append(fa.Field('item_lookup', label="Item Lookup Code", required=True,
validate=self.item_lookup))
@ -277,35 +336,67 @@ class PurchaseBatchView(BatchMasterView):
raise fa.ValidationError("Product not found")
def configure_row_fieldset(self, fs):
try:
batch = self.get_instance()
except httpexceptions.HTTPNotFound:
batch = self.get_row_instance().batch
fs.configure(
include=[
fs.item_lookup,
fs.upc,
fs.product,
fs.cases_ordered,
fs.units_ordered,
fs.cases_received,
fs.units_received,
fs.po_total,
fs.invoice_total,
])
if self.creating:
fs.configure(
include=[
fs.item_lookup,
fs.cases_ordered,
fs.units_ordered,
])
del fs.upc
del fs.product
del fs.po_total
del fs.invoice_total
if batch.mode == self.enum.PURCHASE_BATCH_MODE_NEW:
del fs.cases_received
del fs.units_received
elif batch.mode == self.enum.PURCHASE_BATCH_MODE_RECEIVING:
del fs.cases_ordered
del fs.units_ordered
elif self.editing:
fs.configure(
include=[
fs.upc.readonly(),
fs.product.readonly(),
fs.cases_ordered,
fs.units_ordered,
])
del fs.item_lookup
fs.upc.set(readonly=True)
fs.product.set(readonly=True)
del fs.po_total
del fs.invoice_total
elif self.viewing:
del fs.item_lookup
def before_create_row(self, form):
row = form.fieldset.model
batch = self.get_instance()
row.sequence = max([0] + [r.sequence for r in batch.data_rows]) + 1
row.batch = batch
batch.add_row(row)
# TODO: this seems heavy-handed but works..
row.product_uuid = self.item_lookup(form.fieldset.item_lookup.value)
def after_create_row(self, row):
self.handler.refresh_row(row)
def after_edit_row(self, row):
batch = row.batch
# first undo any totals previously in effect for the row
if batch.mode == self.enum.PURCHASE_BATCH_MODE_NEW and row.po_total:
batch.po_total -= row.po_total
elif batch.mode == self.enum.PURCHASE_BATCH_MODE_RECEIVING and row.invoice_total:
batch.invoice_total -= row.invoice_total
self.handler.refresh_row(row)
def redirect_after_create_row(self, row):
self.request.session.flash("Added item: {} {}".format(row.upc.pretty(), row.product))
return self.redirect(self.request.current_route_url())
@ -478,6 +569,12 @@ class PurchaseBatchView(BatchMasterView):
model_key = cls.get_model_key()
model_title = cls.get_model_title()
# 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))
# defaults
cls._batch_defaults(config)
cls._defaults(config)

View file

@ -45,8 +45,9 @@ class BatchesFieldRenderer(fa.FieldRenderer):
return ''
def render(batch):
return tags.link_to('{} ({})'.format(batch.id_str, enum.PURCHASE_BATCH_MODE[batch.mode]),
self.request.route_url('purchases.batch.view', uuid=batch.uuid))
display = '{} ({}){}'.format(batch.id_str, enum.PURCHASE_BATCH_MODE[batch.mode],
'' if batch.executed else ' (pending)')
return tags.link_to(display, self.request.route_url('purchases.batch.view', uuid=batch.uuid))
enum = self.request.rattail_config.get_enum()
items = [HTML.tag('li', c=render(batch)) for batch in batches]
@ -65,6 +66,17 @@ class PurchaseView(MasterView):
model_row_class = model.PurchaseItem
row_model_title = 'Purchase Item'
def get_instance_title(self, purchase):
if purchase.status >= self.enum.PURCHASE_STATUS_RECEIVED:
if purchase.date_received:
return "{} (received {})".format(purchase.vendor, purchase.date_received.strftime('%Y-%m-%d'))
return "{} (received)".format(purchase.vendor)
elif purchase.status >= self.enum.PURCHASE_STATUS_ORDERED:
if purchase.date_ordered:
return "{} (ordered {})".format(purchase.vendor, purchase.date_ordered.strftime('%Y-%m-%d'))
return "{} (ordered)".format(purchase.vendor)
return unicode(purchase)
def _preconfigure_grid(self, g):
g.joiners['store'] = lambda q: q.join(model.Store)
g.filters['store'] = g.make_filter('store', model.Store.name)
@ -106,7 +118,8 @@ class PurchaseView(MasterView):
fs.status.set(renderer=forms.renderers.EnumFieldRenderer(enum.PURCHASE_STATUS),
readonly=True)
fs.po_number.set(label="PO Number")
fs.po_total.set(label="PO Total")
fs.po_total.set(label="PO Total", renderer=forms.renderers.CurrencyFieldRenderer)
fs.invoice_total.set(renderer=forms.renderers.CurrencyFieldRenderer)
fs.batches.set(renderer=BatchesFieldRenderer)
def configure_fieldset(self, fs):
@ -114,15 +127,24 @@ class PurchaseView(MasterView):
include=[
fs.store,
fs.vendor,
fs.status,
fs.buyer,
fs.date_ordered,
fs.date_received,
fs.po_number,
fs.po_total,
fs.status,
fs.invoice_number,
fs.invoice_total,
fs.created,
fs.created_by,
fs.batches,
])
if self.viewing:
purchase = fs.model
if purchase.status == self.enum.PURCHASE_STATUS_ORDERED:
del fs.date_received
del fs.invoice_number
del fs.invoice_total
def get_parent(self, item):
return item.purchase
@ -136,11 +158,15 @@ class PurchaseView(MasterView):
g.sequence.set(label="Seq")
g.upc.set(label="UPC")
g.brand_name.set(label="Brand")
g.cases_ordered.set(label="Cases")
g.units_ordered.set(label="Units")
g.po_total.set(label="PO Total")
g.cases_ordered.set(label="Cases Ord.")
g.units_ordered.set(label="Units Ord.")
g.cases_received.set(label="Cases Rec.")
g.units_received.set(label="Units Rec.")
g.po_total.set(label="Total", renderer=forms.renderers.CurrencyFieldRenderer)
g.invoice_total.set(label="Total", renderer=forms.renderers.CurrencyFieldRenderer)
def configure_row_grid(self, g):
purchase = self.get_instance()
g.configure(
include=[
g.sequence,
@ -150,15 +176,26 @@ class PurchaseView(MasterView):
g.size,
g.cases_ordered,
g.units_ordered,
g.cases_received,
g.units_received,
g.po_total,
g.invoice_total,
],
readonly=True)
if purchase.status == enum.PURCHASE_STATUS_ORDERED:
del g.cases_received
del g.units_received
del g.invoice_total
elif purchase.status == enum.PURCHASE_STATUS_RECEIVED:
del g.po_total
def _preconfigure_row_fieldset(self, fs):
fs.vendor_code.set(label="Vendor Item Code")
fs.upc.set(label="UPC")
fs.po_unit_cost.set(label="PO Unit Cost")
fs.po_total.set(label="PO Total")
fs.po_unit_cost.set(label="PO Unit Cost", renderer=forms.renderers.CurrencyFieldRenderer)
fs.po_total.set(label="PO Total", renderer=forms.renderers.CurrencyFieldRenderer)
fs.invoice_unit_cost.set(renderer=forms.renderers.CurrencyFieldRenderer)
fs.invoice_total.set(renderer=forms.renderers.CurrencyFieldRenderer)
fs.append(fa.Field('department', value=lambda i: '{} {}'.format(i.department_number, i.department_name)))
def configure_row_fieldset(self, fs):
@ -173,8 +210,12 @@ class PurchaseView(MasterView):
fs.case_quantity,
fs.cases_ordered,
fs.units_ordered,
fs.cases_received,
fs.units_received,
fs.po_unit_cost,
fs.po_total,
fs.invoice_unit_cost,
fs.invoice_total,
])