Add support for new Purchase/Batch views, 'create row' master pattern

More refactoring here but hopefully not that important..
This commit is contained in:
Lance Edgar 2016-11-06 12:58:29 -06:00
parent 8fe0e96273
commit a6e43d1658
11 changed files with 555 additions and 26 deletions

View file

@ -28,6 +28,12 @@
white-space: nowrap;
}
.newgrid-wrapper .grid-header td.tools p {
line-height: 2em;
margin: 0;
padding: 0 0.5em 0 0;
}
/******************************
* filters
******************************/

View file

@ -0,0 +1,10 @@
## -*- coding: utf-8 -*-
<%inherit file="/master/create.mako" />
<%def name="title()">New ${row_model_title}</%def>
<%def name="context_menu_items()">
<li>${h.link_to("Back to {}".format(model_title), index_url)}</li>
</%def>
${parent.body()}

View file

@ -10,7 +10,7 @@
<li>${h.link_to("Delete this {}".format(row_model_title), row_action_url('delete', instance))}</li>
% endif
% if master.rows_creatable and request.has_perm('{}.create'.format(row_permission_prefix)):
<li>${h.link_to("Create a new {}".format(row_model_title), url('{}.create'.format(row_route_prefix)))}</li>
<li>${h.link_to("Create a new {}".format(row_model_title), url('{}.create_row'.format(route_prefix), uuid=row_parent.uuid))}</li>
% endif
</%def>

View file

@ -0,0 +1,11 @@
## -*- coding: utf-8 -*-
<%inherit file="/master/index.mako" />
<%def name="context_menu_items()">
${parent.context_menu_items()}
% if request.has_perm('purchases.batch'):
<li>${h.link_to("Go to Purchases", url('purchases'))}</li>
% endif
</%def>
${parent.body()}

View file

@ -0,0 +1,11 @@
## -*- coding: utf-8 -*-
<%inherit file="/master/index.mako" />
<%def name="context_menu_items()">
${parent.context_menu_items()}
% if request.has_perm('purchases.batch.list'):
<li>${h.link_to("Go to Purchase Batches", url('purchases.batch'))}</li>
% endif
</%def>
${parent.body()}

View file

@ -76,6 +76,7 @@ def includeme(config):
config.include('tailbone.views.people')
config.include('tailbone.views.products')
config.include('tailbone.views.progress')
config.include('tailbone.views.purchases')
config.include('tailbone.views.reportcodes')
config.include('tailbone.views.reports')
config.include('tailbone.views.roles')

View file

@ -182,10 +182,7 @@ class BatchMasterView(MasterView):
views are encouraged to override this method.
"""
if self.creating:
fs.configure(
include=[
fs.created_by.hidden(),
])
fs.configure()
else:
batch = fs.model
@ -208,21 +205,31 @@ class BatchMasterView(MasterView):
def _postconfigure_fieldset(self, fs):
if self.creating:
if 'created' in fs.render_fields:
del fs.created
if 'created_by' in fs.render_fields:
del fs.created_by
if 'executed' in fs.render_fields:
del fs.executed
if 'executed_by' in fs.render_fields:
del fs.executed_by
unwanted = [
'id',
'rowcount',
'created',
'created_by',
'cognized',
'cognized_by',
'executed',
'executed_by',
'purge',
'data_rows',
]
for field in unwanted:
if field in fs.render_fields:
delattr(fs, field)
else:
batch = fs.model
if not batch.executed:
if 'executed' in fs.render_fields:
del fs.executed
if 'executed_by' in fs.render_fields:
del fs.executed_by
unwanted = [
'executed',
'executed_by',
]
for field in unwanted:
if field in fs.render_fields:
delattr(fs, field)
def save_create_form(self, form):
"""
@ -325,11 +332,14 @@ class BatchMasterView(MasterView):
return self.render_to_response('edit', context)
def make_row_grid_tools(self, batch):
def make_batch_row_grid_tools(self, batch):
if not batch.executed:
url = self.request.route_url('{}.delete_rows'.format(self.get_route_prefix()), uuid=batch.uuid)
return HTML.tag('p', c=tags.link_to("Delete all rows matching current search", url))
def make_row_grid_tools(self, batch):
return self.make_batch_row_grid_tools(batch)
def redirect_after_edit(self, batch):
"""
Redirect back to edit batch page after editing a batch, unless the
@ -464,7 +474,7 @@ class BatchMasterView(MasterView):
if progress:
progress.session.load()
progress.session['error'] = True
progress.session['error_msg'] = "Data refresh failed: {}".format(error)
progress.session['error_msg'] = "Data refresh failed: {} {}".format(error.__class__.__name__, error)
progress.session.save()
return
@ -619,7 +629,7 @@ class BatchMasterView(MasterView):
if progress:
progress.session.load()
progress.session['error'] = True
progress.session['error_msg'] = "Batch execution failed: {}".format(error)
progress.session['error_msg'] = "Batch execution failed: {}: {}".format(type(error).__name__, error)
progress.session.save()
# If no error, check result flag (false means user canceled).

View file

@ -36,6 +36,7 @@ from rattail.util import prettify
import formalchemy
from pyramid import httpexceptions
from pyramid.renderers import get_renderer, render_to_response, render
from webhelpers.html import HTML, tags
from tailbone import forms, newgrids as grids
from tailbone.views import View
@ -179,8 +180,14 @@ class MasterView(View):
tools=self.make_row_grid_tools(instance))
return self.render_to_response('view', context)
def make_row_grid_tools(self, instance):
pass
def make_default_row_grid_tools(self, obj):
if self.rows_creatable:
link = tags.link_to("Create a new {}".format(self.get_row_model_title()),
self.get_action_url('create_row', obj))
return HTML.tag('p', c=link)
def make_row_grid_tools(self, obj):
return self.make_default_row_grid_tools(obj)
def make_row_grid(self, **kwargs):
"""
@ -296,6 +303,8 @@ class MasterView(View):
@classmethod
def get_row_model_title(cls):
if hasattr(cls, 'row_model_title'):
return cls.row_model_title
return "{} Row".format(cls.get_model_title())
@classmethod
@ -1022,6 +1031,40 @@ class MasterView(View):
# Associated Rows Stuff
##############################
def create_row(self):
"""
View for creating a new row record.
"""
self.creating = True
parent = self.get_instance()
index_url = self.get_action_url('view', parent)
form = self.make_row_form(self.model_row_class, cancel_url=index_url)
if self.request.method == 'POST':
if form.validate():
self.before_create_row(form)
self.save_create_row_form(form)
obj = form.fieldset.model
self.after_create_row(obj)
return self.redirect_after_create_row(obj)
return self.render_to_response('create_row', {
'index_url': index_url,
'index_title': '{} {}'.format(
self.get_model_title(),
self.get_instance_title(parent)),
'form': form})
def save_create_row_form(self, form):
self.save_row_form(form)
def before_create_row(self, form):
pass
def after_create_row(self, row_object):
pass
def redirect_after_create_row(self, row):
return self.redirect(self.get_action_url('view', self.get_parent(row)))
def view_row(self):
"""
View for viewing details of a single data row.
@ -1061,6 +1104,7 @@ class MasterView(View):
parent = self.get_parent(row)
return self.render_to_response('edit_row', {
'instance': row,
'row_parent': parent,
'instance_title': self.get_row_instance_title(row),
'instance_deletable': self.row_deletable(row),
'index_url': self.get_action_url('view', parent),
@ -1138,10 +1182,11 @@ class MasterView(View):
self.configure_row_fieldset(fieldset)
kwargs.setdefault('action_url', self.request.current_route_url(_query=None))
if 'cancel_url' not in kwargs:
if self.creating:
kwargs.setdefault('cancel_url', self.get_action_url('view', self.get_parent(instance)))
kwargs['cancel_url'] = self.get_action_url('view', self.get_parent(instance))
else:
kwargs.setdefault('cancel_url', self.get_row_action_url('view', instance))
kwargs['cancel_url'] = self.get_row_action_url('view', instance)
form = forms.AlchemyForm(self.request, fieldset, **kwargs)
form.readonly = self.viewing
@ -1253,6 +1298,14 @@ class MasterView(View):
### sub-rows stuff follows
# create row
if cls.has_rows and cls.rows_creatable:
config.add_route('{}.create_row'.format(route_prefix), '{}/{{{}}}/new-row'.format(url_prefix, model_key))
config.add_view(cls, attr='create_row', route_name='{}.create_row'.format(route_prefix),
permission='{}.create_row'.format(permission_prefix))
config.add_tailbone_permission(permission_prefix, '{}.create_row'.format(permission_prefix),
"Create new {} rows".format(model_title))
# view row
if cls.has_rows and cls.rows_viewable:
config.add_route('{}.view'.format(row_route_prefix), '{}/{{uuid}}'.format(row_url_prefix))

View file

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2016 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Views for purchase orders
"""
from __future__ import unicode_literals, absolute_import
def includeme(config):
config.include('tailbone.views.purchases.core')
config.include('tailbone.views.purchases.batch')

View file

@ -0,0 +1,232 @@
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2016 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Views for purchase order batches
"""
from __future__ import unicode_literals, absolute_import
from rattail.db import model, api
from rattail.gpc import GPC
from rattail.db.batch.purchase.handler import PurchaseBatchHandler
from rattail.time import localtime
import formalchemy as fa
from tailbone import forms
from tailbone.db import Session
from tailbone.views.batch import BatchMasterView
class PurchaseBatchView(BatchMasterView):
"""
Master view for purchase order batches.
"""
model_class = model.PurchaseBatch
model_title_plural = "Purchase Batches"
model_row_class = model.PurchaseBatchRow
batch_handler_class = PurchaseBatchHandler
route_prefix = 'purchases.batch'
url_prefix = '/purchases/batches'
rows_creatable = True
rows_editable = True
def _preconfigure_grid(self, g):
super(PurchaseBatchView, self)._preconfigure_grid(g)
g.joiners['vendor'] = lambda q: q.join(model.Vendor)
g.filters['vendor'] = g.make_filter('vendor', model.Vendor.name,
default_active=True, default_verb='contains')
g.sorters['vendor'] = g.make_sorter(model.Vendor.name)
g.joiners['buyer'] = lambda q: q.join(model.Employee).join(model.Person)
g.filters['buyer'] = g.make_filter('buyer', model.Person.display_name,
default_active=True, default_verb='contains')
g.sorters['buyer'] = g.make_sorter(model.Person.display_name)
g.date_ordered.set(label="Ordered")
def configure_grid(self, g):
g.configure(
include=[
g.id,
g.vendor,
g.buyer,
g.date_ordered,
g.created,
g.created_by,
g.executed,
],
readonly=True)
def _preconfigure_fieldset(self, fs):
super(PurchaseBatchView, self)._preconfigure_fieldset(fs)
fs.po_number.set(label="PO Number")
fs.po_total.set(label="PO Total")
def configure_fieldset(self, fs):
fs.configure(
include=[
fs.store,
fs.vendor.with_renderer(forms.renderers.VendorFieldRenderer),
fs.buyer.with_renderer(forms.renderers.EmployeeFieldRenderer),
fs.date_ordered,
fs.po_number,
fs.po_total,
fs.created,
fs.created_by,
fs.executed,
fs.executed_by,
])
if self.creating:
del fs.po_total
# default store may be configured
store = self.rattail_config.get('rattail', 'store')
if store:
store = api.get_store(Session(), store)
if store:
fs.model.store = store
# default buyer is current user
if self.request.method != 'POST':
buyer = self.request.user.employee
if buyer:
fs.model.buyer = buyer
# default order date is today
fs.model.date_ordered = localtime(self.rattail_config).date()
def _preconfigure_row_grid(self, g):
super(PurchaseBatchView, self)._preconfigure_row_grid(g)
g.filters['upc'].label = "UPC"
g.filters['brand_name'].label = "Brand"
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")
def configure_row_grid(self, g):
g.configure(
include=[
g.sequence,
g.upc,
g.brand_name,
g.description,
g.size,
g.cases_ordered,
g.units_ordered,
g.po_total,
g.status_code,
],
readonly=True)
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 _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.append(fa.Field('item_lookup', label="Item Lookup Code", required=True,
validate=self.item_lookup))
def item_lookup(self, value, field=None):
"""
Try to locate a single product using ``value`` as a lookup code.
"""
batch = self.get_instance()
product = api.get_product_by_vendor_code(Session(), value, vendor=batch.vendor)
if product:
return product.uuid
if value.isdigit():
product = api.get_product_by_upc(Session(), GPC(value))
if not product:
product = api.get_product_by_upc(Session(), GPC(value, calc_check_digit='upc'))
if product:
if not product.cost_for_vendor(batch.vendor):
raise fa.ValidationError("Product {} exists but has no cost for vendor {}".format(
product.upc.pretty(), batch.vendor))
return product.uuid
raise fa.ValidationError("Product not found")
def configure_row_fieldset(self, fs):
if self.creating:
fs.configure(
include=[
fs.item_lookup,
fs.cases_ordered,
fs.units_ordered,
])
elif self.editing:
fs.configure(
include=[
fs.upc.readonly(),
fs.product.readonly(),
fs.cases_ordered,
fs.units_ordered,
])
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
# 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 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())
# TODO: redirect to new purchase...
# def get_execute_success_url(self, batch, result, **kwargs):
# # return self.get_action_url('view', batch)
# return
def includeme(config):
PurchaseBatchView.defaults(config)

View file

@ -0,0 +1,163 @@
# -*- coding: utf-8 -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2016 Lance Edgar
#
# This file is part of Rattail.
#
# Rattail is free software: you can redistribute it and/or modify it under the
# terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
# more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Views for "true" purchase orders
"""
from __future__ import unicode_literals, absolute_import
from rattail import enum
from rattail.db import model
import formalchemy as fa
from tailbone import forms
from tailbone.db import Session
from tailbone.views import MasterView
class PurchaseView(MasterView):
"""
Master view for purchase orders.
"""
model_class = model.Purchase
creatable = False
editable = False
has_rows = True
model_row_class = model.PurchaseItem
row_model_title = 'Purchase Item'
def _preconfigure_grid(self, g):
g.joiners['store'] = lambda q: q.join(model.Store)
g.filters['store'] = g.make_filter('store', model.Store.name)
g.sorters['store'] = g.make_sorter(model.Store.name)
g.joiners['vendor'] = lambda q: q.join(model.Vendor)
g.filters['vendor'] = g.make_filter('vendor', model.Vendor.name,
default_active=True, default_verb='contains')
g.sorters['vendor'] = g.make_sorter(model.Vendor.name)
g.joiners['buyer'] = lambda q: q.join(model.Employee).join(model.Person)
g.filters['buyer'] = g.make_filter('buyer', model.Person.display_name,
default_active=True, default_verb='contains')
g.sorters['buyer'] = g.make_sorter(model.Person.display_name)
g.filters['date_ordered'].label = "Ordered"
g.filters['date_ordered'].default_active = True
g.filters['date_ordered'].default_verb = 'equal'
g.default_sortkey = 'date_ordered'
g.default_sortdir = 'desc'
g.date_ordered.set(label="Ordered")
g.status.set(renderer=forms.renderers.EnumFieldRenderer(enum.PURCHASE_STATUS))
def configure_grid(self, g):
g.configure(
include=[
g.store,
g.vendor,
g.buyer,
g.date_ordered,
g.status,
],
readonly=True)
def _preconfigure_fieldset(self, fs):
fs.vendor.set(renderer=forms.renderers.VendorFieldRenderer)
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")
def configure_fieldset(self, fs):
fs.configure(
include=[
fs.store,
fs.vendor,
fs.buyer,
fs.date_ordered,
fs.po_number,
fs.po_total,
fs.status,
fs.created,
fs.created_by,
])
def get_parent(self, item):
return item.purchase
def get_row_data(self, purchase):
return Session.query(model.PurchaseItem)\
.filter(model.PurchaseItem.purchase == purchase)
def _preconfigure_row_grid(self, g):
g.default_sortkey = 'sequence'
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")
def configure_row_grid(self, g):
g.configure(
include=[
g.sequence,
g.upc,
g.brand_name,
g.description,
g.size,
g.cases_ordered,
g.units_ordered,
g.po_total,
],
readonly=True)
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.append(fa.Field('department', value=lambda i: '{} {}'.format(i.department_number, i.department_name)))
def configure_row_fieldset(self, fs):
fs.configure(
include=[
fs.sequence,
fs.vendor_code,
fs.upc,
fs.product,
fs.department,
fs.case_quantity,
fs.cases_ordered,
fs.units_ordered,
fs.po_unit_cost,
fs.po_total,
])
def includeme(config):
PurchaseView.defaults(config)