Add initial support for viewing new purchase batch as Order Form
This commit is contained in:
parent
0477561ca6
commit
758ae7099d
|
@ -36,23 +36,35 @@
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="buttons()">
|
<%def name="buttons()">
|
||||||
## TODO: the refreshable thing still seems confusing...
|
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
% if master.refreshable:
|
${self.leading_buttons()}
|
||||||
% if form.readonly:
|
${refresh_button()}
|
||||||
% if not batch.executed:
|
${execute_button()}
|
||||||
<button type="button" id="refresh-data">Refresh Data</button>
|
|
||||||
% endif
|
|
||||||
% elif batch.refreshable:
|
|
||||||
${h.submit('save-refresh', "Save & Refresh Data")}
|
|
||||||
% endif
|
|
||||||
% endif
|
|
||||||
% if not batch.executed and request.has_perm('{}.execute'.format(permission_prefix)):
|
|
||||||
<button type="button" id="execute-batch"${'' if execute_enabled else ' disabled="disabled"'}>${execute_title}</button>
|
|
||||||
% endif
|
|
||||||
</div>
|
</div>
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
<%def name="leading_buttons()">
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="refresh_button()">
|
||||||
|
## TODO: the refreshable thing still seems confusing...
|
||||||
|
% if master.refreshable:
|
||||||
|
% if form.readonly:
|
||||||
|
% if not batch.executed:
|
||||||
|
<button type="button" id="refresh-data">Refresh Data</button>
|
||||||
|
% endif
|
||||||
|
% elif batch.refreshable:
|
||||||
|
${h.submit('save-refresh', "Save & Refresh Data")}
|
||||||
|
% endif
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="execute_button()">
|
||||||
|
% if not batch.executed and request.has_perm('{}.execute'.format(permission_prefix)):
|
||||||
|
<button type="button" id="execute-batch"${'' if execute_enabled else ' disabled="disabled"'}>${execute_title}</button>
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
<ul id="context-menu">
|
<ul id="context-menu">
|
||||||
${self.context_menu_items()}
|
${self.context_menu_items()}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
161
tailbone/templates/purchases/batches/order_form.mako
Normal file
161
tailbone/templates/purchases/batches/order_form.mako
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
## -*- coding: utf-8 -*-
|
||||||
|
<%inherit file="/base.mako" />
|
||||||
|
|
||||||
|
<%def name="title()">Purchase Order Form</%def>
|
||||||
|
|
||||||
|
<%def name="head_tags()">
|
||||||
|
${parent.head_tags()}
|
||||||
|
${h.javascript_link(request.static_url('tailbone:static/js/numeric.js'))}
|
||||||
|
<script type="text/javascript">
|
||||||
|
$(function() {
|
||||||
|
|
||||||
|
$('.order-form td.current-order input').keydown(function(event) {
|
||||||
|
if (key_allowed(event) || key_modifies(event)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (event.which == 13) {
|
||||||
|
var input = $(this);
|
||||||
|
var row = input.parents('tr:first');
|
||||||
|
var data = {
|
||||||
|
product_uuid: row.data('uuid'),
|
||||||
|
cases_ordered: input.val() || '0'
|
||||||
|
};
|
||||||
|
$.post('${url('purchases.batch.order_form_update', uuid=batch.uuid)}', data, function(data) {
|
||||||
|
if (data.error) {
|
||||||
|
alert(data.error);
|
||||||
|
} else {
|
||||||
|
input.val(data.row_cases_ordered);
|
||||||
|
row.find('td:last').html(data.row_po_total);
|
||||||
|
$('.po-total .field').html(data.batch_po_total);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style type="text/css">
|
||||||
|
|
||||||
|
.order-form th.department {
|
||||||
|
border-top: 1px solid black;
|
||||||
|
font-size: 1.2em;
|
||||||
|
padding: 15px;
|
||||||
|
text-align: left;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-form th.subdepartment {
|
||||||
|
background-color: white;
|
||||||
|
border-bottom: 1px solid black;
|
||||||
|
border-top: 1px solid black;
|
||||||
|
padding: 15px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-form td {
|
||||||
|
border-right: 1px solid #000000;
|
||||||
|
border-top: 1px solid #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-form td.upc,
|
||||||
|
.order-form td.case-qty,
|
||||||
|
.order-form td.code,
|
||||||
|
.order-form td.preferred {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-form td.scratch_pad {
|
||||||
|
width: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-form td.current-order input {
|
||||||
|
text-align: center;
|
||||||
|
width: 3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-form td.po-total {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
|
||||||
|
<%def name="context_menu_items()">
|
||||||
|
<li>${h.link_to("Back to Purchase Batch", url('purchases.batch.view', uuid=batch.uuid))}</li>
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
|
||||||
|
<ul id="context-menu">
|
||||||
|
${self.context_menu_items()}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="form-wrapper">
|
||||||
|
|
||||||
|
<div class="field-wrapper">
|
||||||
|
<label>Vendor</label>
|
||||||
|
<div class="field">${vendor}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field-wrapper">
|
||||||
|
<label>Order Date</label>
|
||||||
|
<div class="field">${batch.date_ordered}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field-wrapper po-total">
|
||||||
|
<label>PO Total</label>
|
||||||
|
<div class="field">$${'{:0,.2f}'.format(batch.po_total or 0)}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div><!-- form-wrapper -->
|
||||||
|
|
||||||
|
|
||||||
|
<div class="newgrid">
|
||||||
|
<table class="order-form">
|
||||||
|
% for department in sorted(departments.itervalues(), key=lambda d: d.name if d else ''):
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="department" colspan="22">Department: ${department.number} ${department.name}</th>
|
||||||
|
</tr>
|
||||||
|
% for subdepartment in sorted(department._order_subdepartments.itervalues(), key=lambda s: s.name if s else ''):
|
||||||
|
<tr>
|
||||||
|
<th class="subdepartment" colspan="22">Subdepartment: ${subdepartment.number} ${subdepartment.name}</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>UPC</th>
|
||||||
|
<th>Brand</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Size</th>
|
||||||
|
<th>Case</th>
|
||||||
|
<th>Vend. Code</th>
|
||||||
|
<th>Pref.</th>
|
||||||
|
<th colspan="7"> </th>
|
||||||
|
<th>PO Total</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
% for cost in subdepartment._order_costs:
|
||||||
|
<tr data-uuid="${cost.product_uuid}">
|
||||||
|
<td class="upc">${get_upc(cost.product)}</td>
|
||||||
|
<td class="brand">${cost.product.brand or ''}</td>
|
||||||
|
<td class="desc">${cost.product.description}</td>
|
||||||
|
<td class="size">${cost.product.size or ''}</td>
|
||||||
|
<td class="case-qty">${cost.case_size} ${"LB" if cost.product.weighed else "EA"}</td>
|
||||||
|
<td class="code">${cost.code or ''}</td>
|
||||||
|
<td class="preferred">${'X' if cost.preference == 1 else ''}</td>
|
||||||
|
% for i in range(6):
|
||||||
|
<td class="scratch_pad"> </td>
|
||||||
|
% endfor
|
||||||
|
<td class="current-order">
|
||||||
|
${h.text('cases_ordered_{}'.format(cost.uuid), value=int(cost._batchrow.cases_ordered) if cost._batchrow else None)}
|
||||||
|
</td>
|
||||||
|
<td class="po-total">${'${:0,.2f}'.format(cost._batchrow.po_total) if cost._batchrow else ''}</td>
|
||||||
|
</tr>
|
||||||
|
% endfor
|
||||||
|
</tbody>
|
||||||
|
% endfor
|
||||||
|
% endfor
|
||||||
|
</table>
|
||||||
|
</div>
|
29
tailbone/templates/purchases/batches/view.mako
Normal file
29
tailbone/templates/purchases/batches/view.mako
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
## -*- coding: utf-8 -*-
|
||||||
|
<%inherit file="/newbatch/view.mako" />
|
||||||
|
|
||||||
|
<%def name="head_tags()">
|
||||||
|
${parent.head_tags()}
|
||||||
|
<script type="text/javascript">
|
||||||
|
$(function() {
|
||||||
|
$('#order-form').click(function() {
|
||||||
|
% if vendor_cost_count > 199:
|
||||||
|
if (! confirm("This vendor has ${'{:,d}'.format(vendor_cost_count)} cost records.\n\n" +
|
||||||
|
"It is not recommended to use Order Form mode for such a large catalog.\n\n" +
|
||||||
|
"Are you sure you wish to do it anyway?")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
% endif
|
||||||
|
$(this).button('disable').button('option', 'label', "Working, please wait...");
|
||||||
|
location.href = '${url('purchases.batch.order_form', uuid=instance.uuid)}';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="leading_buttons()">
|
||||||
|
% if not instance.executed and request.has_perm('purchases.batch.order_form'):
|
||||||
|
<button type="button" id="order-form">View as Order Form</button>
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
${parent.body()}
|
|
@ -30,6 +30,7 @@ from rattail.db import model, api
|
||||||
from rattail.gpc import GPC
|
from rattail.gpc import GPC
|
||||||
from rattail.db.batch.purchase.handler import PurchaseBatchHandler
|
from rattail.db.batch.purchase.handler import PurchaseBatchHandler
|
||||||
from rattail.time import localtime
|
from rattail.time import localtime
|
||||||
|
from rattail.core import Object
|
||||||
|
|
||||||
import formalchemy as fa
|
import formalchemy as fa
|
||||||
|
|
||||||
|
@ -118,6 +119,14 @@ class PurchaseBatchView(BatchMasterView):
|
||||||
# default order date is today
|
# default order date is today
|
||||||
fs.model.date_ordered = localtime(self.rattail_config).date()
|
fs.model.date_ordered = localtime(self.rattail_config).date()
|
||||||
|
|
||||||
|
def template_kwargs_view(self, **kwargs):
|
||||||
|
kwargs = super(PurchaseBatchView, self).template_kwargs_view(**kwargs)
|
||||||
|
vendor = kwargs['batch'].vendor
|
||||||
|
kwargs['vendor_cost_count'] = Session.query(model.ProductCost)\
|
||||||
|
.filter(model.ProductCost.vendor == vendor)\
|
||||||
|
.count()
|
||||||
|
return kwargs
|
||||||
|
|
||||||
def _preconfigure_row_grid(self, g):
|
def _preconfigure_row_grid(self, g):
|
||||||
super(PurchaseBatchView, self)._preconfigure_row_grid(g)
|
super(PurchaseBatchView, self)._preconfigure_row_grid(g)
|
||||||
|
|
||||||
|
@ -222,10 +231,150 @@ class PurchaseBatchView(BatchMasterView):
|
||||||
self.request.session.flash("Added item: {} {}".format(row.upc.pretty(), row.product))
|
self.request.session.flash("Added item: {} {}".format(row.upc.pretty(), row.product))
|
||||||
return self.redirect(self.request.current_route_url())
|
return self.redirect(self.request.current_route_url())
|
||||||
|
|
||||||
|
def delete_row(self):
|
||||||
|
"""
|
||||||
|
Update the PO total in addition to marking row as removed.
|
||||||
|
"""
|
||||||
|
row = self.Session.query(self.model_row_class).get(self.request.matchdict['uuid'])
|
||||||
|
if not row:
|
||||||
|
raise httpexceptions.HTTPNotFound()
|
||||||
|
if row.po_total:
|
||||||
|
row.batch.po_total -= row.po_total
|
||||||
|
row.removed = True
|
||||||
|
return self.redirect(self.get_action_url('view', row.batch))
|
||||||
|
|
||||||
# TODO: redirect to new purchase...
|
# TODO: redirect to new purchase...
|
||||||
# def get_execute_success_url(self, batch, result, **kwargs):
|
# def get_execute_success_url(self, batch, result, **kwargs):
|
||||||
# # return self.get_action_url('view', batch)
|
# # return self.get_action_url('view', batch)
|
||||||
# return
|
# return
|
||||||
|
|
||||||
|
def order_form(self):
|
||||||
|
"""
|
||||||
|
View for editing a purchase batch as an order form.
|
||||||
|
"""
|
||||||
|
batch = self.get_instance()
|
||||||
|
vendor = batch.vendor
|
||||||
|
costs = Session.query(model.ProductCost)\
|
||||||
|
.join(model.Product)\
|
||||||
|
.outerjoin(model.Brand)\
|
||||||
|
.filter(model.ProductCost.vendor == vendor)\
|
||||||
|
.order_by(model.Brand.name,
|
||||||
|
model.Product.description,
|
||||||
|
model.Product.size)
|
||||||
|
|
||||||
|
# organize existing batch rows by product
|
||||||
|
order_items = {}
|
||||||
|
for row in batch.data_rows:
|
||||||
|
if not row.removed:
|
||||||
|
order_items[row.product_uuid] = row
|
||||||
|
|
||||||
|
# organize product costs by dept / subdept
|
||||||
|
departments = {}
|
||||||
|
for cost in costs:
|
||||||
|
|
||||||
|
department = cost.product.department
|
||||||
|
if department:
|
||||||
|
departments.setdefault(department.uuid, department)
|
||||||
|
else:
|
||||||
|
if None not in departments:
|
||||||
|
department = Object()
|
||||||
|
departments[None] = department
|
||||||
|
department = departments[None]
|
||||||
|
|
||||||
|
subdepartments = getattr(department, '_order_subdepartments', None)
|
||||||
|
if subdepartments is None:
|
||||||
|
subdepartments = department._order_subdepartments = {}
|
||||||
|
|
||||||
|
subdepartment = cost.product.subdepartment
|
||||||
|
if subdepartment:
|
||||||
|
subdepartments.setdefault(subdepartment.uuid, subdepartment)
|
||||||
|
else:
|
||||||
|
if None not in subdepartments:
|
||||||
|
subdepartment = Object()
|
||||||
|
subdepartments[None] = subdepartment
|
||||||
|
subdepartment = subdepartments[None]
|
||||||
|
|
||||||
|
subdept_costs = getattr(subdepartment, '_order_costs', None)
|
||||||
|
if subdept_costs is None:
|
||||||
|
subdept_costs = subdepartment._order_costs = []
|
||||||
|
subdept_costs.append(cost)
|
||||||
|
cost._batchrow = order_items.get(cost.product_uuid)
|
||||||
|
|
||||||
|
title = self.get_instance_title(batch)
|
||||||
|
return self.render_to_response('order_form', {
|
||||||
|
'batch': batch,
|
||||||
|
'instance': batch,
|
||||||
|
'instance_title': title,
|
||||||
|
'index_title': "{}: {}".format(self.get_model_title(), title),
|
||||||
|
'index_url': self.get_action_url('view', batch),
|
||||||
|
'vendor': vendor,
|
||||||
|
'departments': departments,
|
||||||
|
'get_upc': lambda p: p.upc,
|
||||||
|
})
|
||||||
|
|
||||||
|
def order_form_update(self):
|
||||||
|
"""
|
||||||
|
Handles AJAX requests to update current batch, from Order Form view.
|
||||||
|
"""
|
||||||
|
batch = self.get_instance()
|
||||||
|
|
||||||
|
quantity = self.request.POST.get('cases_ordered')
|
||||||
|
if not quantity or not quantity.isdigit():
|
||||||
|
return {'error': "Invalid quantity: {}".format(quantity)}
|
||||||
|
quantity = int(quantity)
|
||||||
|
|
||||||
|
uuid = self.request.POST.get('product_uuid')
|
||||||
|
product = Session.query(model.Product).get(uuid) if uuid else None
|
||||||
|
if not product:
|
||||||
|
return {'error': "Product not found"}
|
||||||
|
|
||||||
|
rows = [row for row in batch.data_rows if row.product_uuid == uuid]
|
||||||
|
if rows:
|
||||||
|
assert len(rows) == 1
|
||||||
|
row = rows[0]
|
||||||
|
if row.po_total and not row.removed:
|
||||||
|
batch.po_total -= row.po_total
|
||||||
|
if quantity:
|
||||||
|
row.cases_ordered = quantity
|
||||||
|
row.removed = False
|
||||||
|
self.handler.refresh_row(row)
|
||||||
|
else:
|
||||||
|
row.removed = True
|
||||||
|
|
||||||
|
elif quantity:
|
||||||
|
row = model.PurchaseBatchRow()
|
||||||
|
row.sequence = max([0] + [r.sequence for r in batch.data_rows]) + 1
|
||||||
|
row.product = product
|
||||||
|
batch.data_rows.append(row)
|
||||||
|
row.cases_ordered = quantity
|
||||||
|
self.handler.refresh_row(row)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'row_cases_ordered': '' if row.removed else int(row.cases_ordered),
|
||||||
|
'row_po_total': '' if row.removed else '${:0,.2f}'.format(row.po_total),
|
||||||
|
'batch_po_total': '${:0,.2f}'.format(batch.po_total),
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def defaults(cls, config):
|
||||||
|
route_prefix = cls.get_route_prefix()
|
||||||
|
url_prefix = cls.get_url_prefix()
|
||||||
|
permission_prefix = cls.get_permission_prefix()
|
||||||
|
model_key = cls.get_model_key()
|
||||||
|
model_title = cls.get_model_title()
|
||||||
|
|
||||||
|
cls._batch_defaults(config)
|
||||||
|
cls._defaults(config)
|
||||||
|
|
||||||
|
# order form
|
||||||
|
config.add_tailbone_permission(permission_prefix, '{}.order_form'.format(permission_prefix),
|
||||||
|
"Edit new {} in Order Form mode".format(model_title))
|
||||||
|
config.add_route('{}.order_form'.format(route_prefix), '{}/{{{}}}/order-form'.format(url_prefix, model_key))
|
||||||
|
config.add_view(cls, attr='order_form', route_name='{}.order_form'.format(route_prefix),
|
||||||
|
permission='{}.order_form'.format(permission_prefix))
|
||||||
|
config.add_route('{}.order_form_update'.format(route_prefix), '{}/{{{}}}/order-form/update'.format(url_prefix, model_key))
|
||||||
|
config.add_view(cls, attr='order_form_update', route_name='{}.order_form_update'.format(route_prefix),
|
||||||
|
renderer='json', permission='{}.order_form'.format(permission_prefix))
|
||||||
|
|
||||||
|
|
||||||
def includeme(config):
|
def includeme(config):
|
||||||
|
|
Loading…
Reference in a new issue