diff --git a/tailbone/templates/batch/crud.mako b/tailbone/templates/batch/crud.mako
new file mode 100644
index 00000000..b7ed9353
--- /dev/null
+++ b/tailbone/templates/batch/crud.mako
@@ -0,0 +1,78 @@
+## -*- coding: utf-8 -*-
+<%inherit file="/crud.mako" />
+
+<%def name="title()">${"View" if form.readonly else "Edit"} ${batch_display}%def>
+
+<%def name="head_tags()">
+
+
+%def>
+
+
+
+
+
+ ${form.render(form_id='batch-form', buttons=capture(buttons))|n}
+
+
+
+<%def name="buttons()">
+
+ % if not form.readonly and batch.refreshable:
+ ${h.submit('save-refresh', "Save & Refresh Data")}
+ % endif
+ % if not batch.executed and request.has_perm('{0}.execute'.format(permission_prefix)):
+ ## ${h.link_to(execute_title, url('{0}.execute'.format(route_prefix), uuid=batch.uuid))}
+
+ % endif
+
+%def>
+
+
diff --git a/tailbone/templates/batch/edit.mako b/tailbone/templates/batch/edit.mako
new file mode 100644
index 00000000..d6922b7c
--- /dev/null
+++ b/tailbone/templates/batch/edit.mako
@@ -0,0 +1,3 @@
+## -*- coding: utf-8 -*-
+<%inherit file="/batch/crud.mako" />
+${parent.body()}
diff --git a/tailbone/templates/batch/rows.mako b/tailbone/templates/batch/rows.mako
index c7d04e57..616fc694 100644
--- a/tailbone/templates/batch/rows.mako
+++ b/tailbone/templates/batch/rows.mako
@@ -9,7 +9,10 @@
- ${h.link_to("Delete all rows matching current search", url('{0}.rows.bulk_delete'.format(route_prefix), uuid=batch.uuid))}
+ ## TODO: Fix this check for edit mode.
+ % if not batch.executed and request.referrer.endswith('/edit'):
+ ${h.link_to("Delete all rows matching current search", url('{0}.rows.bulk_delete'.format(route_prefix), uuid=batch.uuid))}
+ % endif
|
diff --git a/tailbone/templates/batch/view.mako b/tailbone/templates/batch/view.mako
index 63e5e16d..d6922b7c 100644
--- a/tailbone/templates/batch/view.mako
+++ b/tailbone/templates/batch/view.mako
@@ -1,65 +1,3 @@
## -*- coding: utf-8 -*-
-<%inherit file="/crud.mako" />
-
-<%def name="title()">View ${batch_display}%def>
-
-<%def name="head_tags()">
-
-
-%def>
-
-
-
-
-
- ${form.render()|n}
-
-
-
-
+<%inherit file="/batch/crud.mako" />
+${parent.body()}
diff --git a/tailbone/templates/forms/form.mako b/tailbone/templates/forms/form.mako
index e1407fc3..60cb633a 100644
--- a/tailbone/templates/forms/form.mako
+++ b/tailbone/templates/forms/form.mako
@@ -1,16 +1,20 @@
## -*- coding: utf-8 -*-
diff --git a/tailbone/templates/forms/form_readonly.mako b/tailbone/templates/forms/form_readonly.mako
index f8715630..0e4a73f8 100644
--- a/tailbone/templates/forms/form_readonly.mako
+++ b/tailbone/templates/forms/form_readonly.mako
@@ -1,4 +1,7 @@
## -*- coding: utf-8 -*-
${form.fieldset.render()|n}
+ % if buttons:
+ ${buttons|n}
+ % endif
diff --git a/tailbone/templates/vendors/invoices/create.mako b/tailbone/templates/vendors/invoices/create.mako
new file mode 100644
index 00000000..2e46901c
--- /dev/null
+++ b/tailbone/templates/vendors/invoices/create.mako
@@ -0,0 +1,3 @@
+## -*- coding: utf-8 -*-
+<%inherit file="/batch/create.mako" />
+${parent.body()}
diff --git a/tailbone/templates/vendors/invoices/edit.mako b/tailbone/templates/vendors/invoices/edit.mako
new file mode 100644
index 00000000..d0eea0a6
--- /dev/null
+++ b/tailbone/templates/vendors/invoices/edit.mako
@@ -0,0 +1,3 @@
+## -*- coding: utf-8 -*-
+<%inherit file="/batch/edit.mako" />
+${parent.body()}
diff --git a/tailbone/templates/vendors/invoices/index.mako b/tailbone/templates/vendors/invoices/index.mako
new file mode 100644
index 00000000..acddd2fb
--- /dev/null
+++ b/tailbone/templates/vendors/invoices/index.mako
@@ -0,0 +1,3 @@
+## -*- coding: utf-8 -*-
+<%inherit file="/batch/index.mako" />
+${parent.body()}
diff --git a/tailbone/templates/vendors/invoices/view.mako b/tailbone/templates/vendors/invoices/view.mako
new file mode 100644
index 00000000..9b89af91
--- /dev/null
+++ b/tailbone/templates/vendors/invoices/view.mako
@@ -0,0 +1,3 @@
+## -*- coding: utf-8 -*-
+<%inherit file="/batch/view.mako" />
+${parent.body()}
diff --git a/tailbone/views/batch.py b/tailbone/views/batch.py
index 563540fc..c1bf97e7 100644
--- a/tailbone/views/batch.py
+++ b/tailbone/views/batch.py
@@ -37,7 +37,7 @@ import formalchemy
from pyramid.renderers import render_to_response
from pyramid.response import FileResponse
from pyramid.httpexceptions import HTTPFound, HTTPNotFound
-from webhelpers.html.tags import link_to
+from webhelpers.html.tags import link_to, HTML
from rattail.db import model
from rattail.db import Session as RatSession
@@ -234,9 +234,9 @@ class BatchGrid(BaseGrid):
if self.request.has_perm('{0}.view'.format(self.permission_prefix)):
g.viewable = True
g.view_route_name = '{0}.view'.format(self.route_prefix)
- # if self.request.has_perm('{0}.edit'.format(self.permission_prefix)):
- # g.editable = True
- # g.edit_route_name = '{0}.edit'.format(self.route_prefix)
+ if self.request.has_perm('{0}.edit'.format(self.permission_prefix)):
+ g.editable = True
+ g.edit_route_name = '{0}.edit'.format(self.route_prefix)
if self.request.has_perm('{0}.delete'.format(self.permission_prefix)):
g.deletable = True
g.delete_route_name = '{0}.delete'.format(self.route_prefix)
@@ -321,6 +321,12 @@ class BaseCrud(CrudView):
else:
super(BaseCrud, self).flash_create(model)
+ def flash_update(self, model):
+ if 'update' in self.flash:
+ self.request.session.flash(self.flash['update'])
+ else:
+ super(BaseCrud, self).flash_update(model)
+
def flash_delete(self, model):
if 'delete' in self.flash:
self.request.session.flash(self.flash['delete'])
@@ -414,6 +420,33 @@ class BatchCrud(BaseCrud):
fs.executed_by,
])
+ def update(self):
+ """
+ Don't allow editing a batch which has already been executed.
+ """
+ batch = self.get_model_from_request()
+ if not batch:
+ return HTTPNotFound()
+ if batch.executed:
+ return HTTPFound(location=self.view_url(batch.uuid))
+ return self.crud(batch)
+
+ def post_create_url(self, form):
+ """
+ Redirect to view batch after creating a batch.
+ """
+ batch = form.fieldset.model
+ return self.view_url(batch.uuid)
+
+ def post_update_url(self, form):
+ """
+ Redirect back to edit batch page after editing a batch, unless the
+ refresh flag is set, in which case do that.
+ """
+ if self.request.params.get('refresh') == 'true':
+ return self.refresh_url()
+ return self.request.current_route_url()
+
def template_kwargs(self, form):
"""
Add some things to the template context: current batch model, batch
@@ -425,6 +458,7 @@ class BatchCrud(BaseCrud):
'batch': batch,
'batch_display': self.batch_display,
'batch_display_plural': self.batch_display_plural,
+ 'execute_title': self.handler.get_execute_title(batch),
'route_prefix': self.route_prefix,
'permission_prefix': self.permission_prefix,
}
@@ -511,6 +545,14 @@ class BatchCrud(BaseCrud):
uuid = self.request.matchdict['uuid']
return self.request.route_url('{0}.view'.format(self.route_prefix), uuid=uuid)
+ def refresh_url(self, uuid=None):
+ """
+ Returns the URL for refreshing a batch; defaults to current batch.
+ """
+ if uuid is None:
+ uuid = self.request.matchdict['uuid']
+ return self.request.route_url('{0}.refresh'.format(self.route_prefix), uuid=uuid)
+
def execute(self):
batch = self.current_batch()
if self.handler.execute(batch):
@@ -561,15 +603,15 @@ class FileBatchCrud(BatchCrud):
override this, but :meth:`configure_fieldset()` instead.
"""
fs = self.make_fieldset(model)
- fs.created.set(label="Uploaded", renderer=DateTimeFieldRenderer(self.request.rattail_config))
- fs.created_by.set(label="Uploaded by", renderer=UserFieldRenderer)
+ fs.created.set(label="Uploaded", renderer=DateTimeFieldRenderer(self.request.rattail_config), readonly=True)
+ fs.created_by.set(label="Uploaded by", renderer=UserFieldRenderer, readonly=True)
fs.cognized.set(renderer=DateTimeFieldRenderer(self.request.rattail_config))
fs.cognized_by.set(label="Cognized by", renderer=UserFieldRenderer)
fs.executed.set(renderer=DateTimeFieldRenderer(self.request.rattail_config))
fs.executed_by.set(label="Executed by", renderer=UserFieldRenderer)
fs.append(formalchemy.Field('data_file'))
fs.data_file.set(renderer=formalchemy.fields.FileFieldRenderer)
- fs.filename.set(renderer=DownloadLinkRenderer(self.route_prefix))
+ fs.filename.set(renderer=DownloadLinkRenderer(self.route_prefix), readonly=True)
self.configure_fieldset(fs)
if self.creating:
del fs.created
@@ -660,13 +702,6 @@ class FileBatchCrud(BatchCrud):
return HTTPFound(location=self.request.route_url(
'{0}.create'.format(self.route_prefix)))
- def post_save_url(self, form):
- """
- Redirect to "view batch" after creating or updating a batch.
- """
- batch = form.fieldset.model
- return self.view_url(batch.uuid)
-
def pre_delete(self, batch):
"""
Delete all data (files etc.) for the batch.
@@ -690,6 +725,23 @@ class FileBatchCrud(BatchCrud):
return response
+class StatusRenderer(EnumFieldRenderer):
+ """
+ Custom renderer for ``status_code`` fields. Adds ``status_text`` value as
+ title attribute if it exists.
+ """
+
+ def render_readonly(self, **kwargs):
+ value = self.raw_value
+ if value is None:
+ return ''
+ status_code_text = self.enumeration.get(value, unicode(value))
+ row = self.field.parent.model
+ if row.status_text:
+ return HTML.tag('span', title=row.status_text, c=status_code_text)
+ return status_code_text
+
+
class BatchRowGrid(BaseGrid):
"""
Base grid view for batch rows, which can be filtered and sorted. Also it
@@ -778,14 +830,16 @@ class BatchRowGrid(BaseGrid):
g = self.make_grid()
g.extra_row_class = self.tr_class
g.sequence.set(label="Seq.")
- g.status_code.set(label="Status", renderer=EnumFieldRenderer(self.row_class.STATUS))
+ g.status_code.set(label="Status", renderer=StatusRenderer(self.row_class.STATUS))
self._configure_grid(g)
self.configure_grid(g)
batch = self.current_batch()
# g.viewable = True
# g.view_route_name = '{0}.rows.view'.format(self.route_prefix)
- if not batch.executed and self.request.has_perm('{0}.edit'.format(self.permission_prefix)):
+ # TODO: Fix this check for edit mode.
+ edit_mode = self.request.referrer.endswith('/edit')
+ if edit_mode and not batch.executed and self.request.has_perm('{0}.edit'.format(self.permission_prefix)):
# g.editable = True
# g.edit_route_name = '{0}.rows.edit'.format(self.route_prefix)
g.deletable = True
diff --git a/tailbone/views/crud.py b/tailbone/views/crud.py
index ce2f6203..31f03b7d 100644
--- a/tailbone/views/crud.py
+++ b/tailbone/views/crud.py
@@ -158,7 +158,11 @@ class CrudView(View):
and self.request.params.get('create_and_continue')):
return HTTPFound(location=self.request.current_route_url())
- return HTTPFound(location=self.post_save_url(form))
+ if form.creating:
+ url = self.post_create_url(form)
+ else:
+ url = self.post_update_url(form)
+ return HTTPFound(location=url)
self.validation_failed(form)
@@ -199,6 +203,12 @@ class CrudView(View):
def post_save_url(self, form):
return self.home_url
+ def post_create_url(self, form):
+ return self.post_save_url(form)
+
+ def post_update_url(self, form):
+ return self.post_save_url(form)
+
def validation_failed(self, form):
pass
diff --git a/tailbone/views/vendors/__init__.py b/tailbone/views/vendors/__init__.py
index f41ba844..79e9d0f3 100644
--- a/tailbone/views/vendors/__init__.py
+++ b/tailbone/views/vendors/__init__.py
@@ -33,3 +33,4 @@ from .core import (VendorsGrid, VendorCrud, VendorVersionView,
def includeme(config):
config.include('tailbone.views.vendors.core')
config.include('tailbone.views.vendors.catalogs')
+ config.include('tailbone.views.vendors.invoices')
diff --git a/tailbone/views/vendors/catalogs.py b/tailbone/views/vendors/catalogs.py
index 8aabc98b..46439a01 100644
--- a/tailbone/views/vendors/catalogs.py
+++ b/tailbone/views/vendors/catalogs.py
@@ -26,8 +26,6 @@ Views for maintaining vendor catalogs
from __future__ import unicode_literals
-from pyramid.httpexceptions import HTTPFound
-
from rattail.db import model
from rattail.db.api import get_setting, get_vendor
from rattail.db.batch.vendorcatalog import VendorCatalog, VendorCatalogRow
diff --git a/tailbone/views/vendors/invoices.py b/tailbone/views/vendors/invoices.py
new file mode 100644
index 00000000..b3df35a4
--- /dev/null
+++ b/tailbone/views/vendors/invoices.py
@@ -0,0 +1,191 @@
+# -*- coding: utf-8 -*-
+################################################################################
+#
+# Rattail -- Retail Software Framework
+# Copyright © 2010-2015 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 .
+#
+################################################################################
+"""
+Views for maintaining vendor invoices
+"""
+
+from __future__ import unicode_literals
+
+from rattail.db import model
+from rattail.db.api import get_setting, get_vendor
+from rattail.db.batch.vendorinvoice import VendorInvoice, VendorInvoiceRow
+from rattail.db.batch.vendorinvoice.handler import VendorInvoiceHandler
+from rattail.vendors.invoices import iter_invoice_parsers, require_invoice_parser
+from rattail.util import load_object
+
+import formalchemy
+
+from tailbone.db import Session
+from tailbone.views.batch import FileBatchGrid, FileBatchCrud, BatchRowGrid, BatchRowCrud, defaults
+
+
+class VendorInvoiceGrid(FileBatchGrid):
+ """
+ Grid view for vendor invoices.
+ """
+ batch_class = VendorInvoice
+ batch_display = "Vendor Invoice"
+ route_prefix = 'vendors.invoices'
+
+ def join_map_extras(self):
+ return {'vendor': lambda q: q.join(model.Vendor)}
+
+ def filter_map_extras(self):
+ return {'vendor': self.filter_ilike(model.Vendor.name)}
+
+ def filter_config_extras(self):
+ return {'filter_type_vendor': 'lk',
+ 'include_filter_vendor': True}
+
+ def sort_map_extras(self):
+ return {'vendor': self.sorter(model.Vendor.name)}
+
+ def configure_grid(self, g):
+ g.configure(
+ include=[
+ g.created,
+ g.created_by,
+ g.vendor,
+ g.filename,
+ g.executed,
+ ],
+ readonly=True)
+
+
+class VendorInvoiceCrud(FileBatchCrud):
+ """
+ CRUD view for vendor invoices.
+ """
+ batch_class = VendorInvoice
+ batch_handler_class = VendorInvoiceHandler
+ route_prefix = 'vendors.invoices'
+
+ batch_display = "Vendor Invoice"
+ flash = {'create': "New vendor invoice has been uploaded.",
+ 'update': "Vendor invoice has been updated.",
+ 'delete': "Vendor invoice has been deleted."}
+
+ def get_handler(self):
+ """
+ Returns a `BatchHandler` instance for the view.
+
+ Derived classes may override this, but if you only need to replace the
+ handler (i.e. and not the view logic) then you can instead subclass
+ :class:`rattail.db.batch.vendorinvoice.handler.VendorInvoiceHandler`
+ and create a setting named "rattail.batch.vendorinvoice.handler" in the
+ database, the value of which should be a spec string pointed at your
+ custom handler.
+ """
+ handler = get_setting(Session, 'rattail.batch.vendorinvoice.handler')
+ if not handler:
+ handler = self.request.rattail_config.get('rattail.batch', 'vendorinvoice.handler')
+ if handler:
+ handler = load_object(handler)(self.request.rattail_config)
+ if not handler:
+ handler = super(VendorInvoiceCrud, self).get_handler()
+ return handler
+
+ def configure_fieldset(self, fs):
+ parsers = sorted(iter_invoice_parsers(), key=lambda p: p.display)
+ parser_options = [(p.display, p.key) for p in parsers]
+ parser_options.insert(0, ("(please choose)", ''))
+ fs.parser_key.set(renderer=formalchemy.fields.SelectFieldRenderer,
+ options=parser_options)
+ fs.configure(
+ include=[
+ fs.vendor.readonly(),
+ fs.data_file.label("Invoice File"),
+ fs.parser_key.label("File Type"),
+ fs.filename,
+ fs.purchase_order_number.label(self.handler.po_number_title),
+ fs.invoice_date.readonly(),
+ fs.created,
+ fs.created_by,
+ fs.executed,
+ fs.executed_by,
+ ])
+ if self.creating:
+ del fs.vendor
+ del fs.invoice_date
+ else:
+ del fs.parser_key
+
+ def init_batch(self, batch):
+ parser = require_invoice_parser(batch.parser_key)
+ vendor = get_vendor(Session, parser.vendor_key)
+ if not vendor:
+ self.request.session.flash("No vendor setting found in database for key: {0}".format(parser.vendor_key))
+ return False
+ batch.vendor = vendor
+ return True
+
+
+class VendorInvoiceRowGrid(BatchRowGrid):
+ """
+ Grid view for vendor invoice rows.
+ """
+ row_class = VendorInvoiceRow
+ route_prefix = 'vendors.invoices'
+
+ def filter_map_extras(self):
+ return {'ilike': ['upc', 'brand_name', 'description', 'size', 'vendor_code']}
+
+ def filter_config_extras(self):
+ return {'filter_label_upc': "UPC",
+ 'filter_label_brand_name': "Brand"}
+
+ def configure_grid(self, g):
+ g.configure(
+ include=[
+ g.sequence,
+ g.upc.label("UPC"),
+ g.brand_name.label("Brand"),
+ g.description,
+ g.size,
+ g.vendor_code,
+ g.shipped_cases.label("Cases"),
+ g.shipped_units.label("Units"),
+ g.unit_cost,
+ g.status_code,
+ ],
+ readonly=True)
+
+ def tr_class(self, row, i):
+ if row.status_code in ((row.STATUS_NOT_IN_PURCHASE,
+ row.STATUS_NOT_IN_INVOICE,
+ row.STATUS_DIFFERS_FROM_PURCHASE)):
+ return 'notice'
+ if row.status_code == row.STATUS_NOT_IN_DB:
+ return 'warning'
+
+
+class VendorInvoiceRowCrud(BatchRowCrud):
+ row_class = VendorInvoiceRow
+ route_prefix = 'vendors.invoices'
+
+
+def includeme(config):
+ """
+ Add configuration for the vendor invoice views.
+ """
+ defaults(config, VendorInvoiceGrid, VendorInvoiceCrud, VendorInvoiceRowGrid, VendorInvoiceRowCrud, '/vendors/invoices/')