tailbone/tailbone/views/batch/vendorcatalog.py
Lance Edgar b4816c6289 Share some code for validating vendor field
and add validation for new Ordering batch
2023-05-30 13:25:20 -05:00

446 lines
15 KiB
Python

# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2023 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 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 General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# Rattail. If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Views for maintaining vendor catalogs
"""
import logging
from rattail.db import model
import colander
from deform import widget as dfwidget
from webhelpers2.html import tags
from tailbone import forms
from tailbone.views.batch import FileBatchMasterView
from tailbone.diffs import Diff
from tailbone.db import Session
log = logging.getLogger(__name__)
class VendorCatalogView(FileBatchMasterView):
"""
Master view for vendor catalog batches.
"""
model_class = model.VendorCatalogBatch
model_row_class = model.VendorCatalogBatchRow
default_handler_spec = 'rattail.batch.vendorcatalog:VendorCatalogHandler'
route_prefix = 'vendorcatalogs'
url_prefix = '/vendors/catalogs'
template_prefix = '/batch/vendorcatalog'
bulk_deletable = True
results_executable = True
rows_bulk_deletable = True
has_input_file_templates = True
configurable = True
labels = {
'vendor_id': "Vendor ID",
'parser_key': "Parser",
}
grid_columns = [
'id',
'vendor',
'description',
'filename',
'rowcount',
'created',
'executed',
]
form_fields = [
'id',
'filename',
'parser_key',
'vendor',
'future',
'effective',
'cache_products',
'params',
'description',
'notes',
'created',
'created_by',
'rowcount',
'executed',
'executed_by',
]
row_grid_columns = [
'sequence',
'upc',
'brand_name',
'description',
'size',
'vendor_code',
'is_preferred_vendor',
'old_unit_cost',
'unit_cost',
'unit_cost_diff',
'unit_cost_diff_percent',
'starts',
'status_code',
]
row_form_fields = [
'sequence',
'item_entry',
'product',
'upc',
'brand_name',
'description',
'size',
'is_preferred_vendor',
'suggested_retail',
'starts',
'ends',
'discount_starts',
'discount_ends',
'discount_amount',
'discount_percent',
'case_cost_diff',
'unit_cost_diff',
'status_code',
'status_text',
]
def get_input_file_templates(self):
return [
{'key': 'default',
'label': "Default",
'default_url': self.request.static_url(
'tailbone:static/files/vendor_catalog_template.xlsx')},
]
def get_parsers(self):
if not hasattr(self, 'parsers'):
app = self.get_rattail_app()
vendor_handler = app.get_vendor_handler()
self.parsers = vendor_handler.get_supported_catalog_parsers()
return self.parsers
def configure_grid(self, g):
super(VendorCatalogView, self).configure_grid(g)
model = self.model
# nb. this batch has vendor_id and vendor_name fields, but in
# practice they aren't used much and normally just the vendor
# proper is set. so we remove simple filters and add the
# custom one for (referenced) vendor name
g.remove_filter('vendor_id')
g.remove_filter('vendor_name')
g.set_joiner('vendor', lambda q: q.join(model.Vendor))
g.set_filter('vendor', model.Vendor.name,
default_active=True,
default_verb='contains')
# and this is the same thing we are showing as grid column
g.set_sorter('vendor', model.Vendor.name)
# preferred filters
g.set_filters_sequence([
'id',
'vendor',
'description',
'executed',
'created',
'filename',
'future',
'effective',
'notes',
])
g.set_link('vendor')
g.set_link('filename')
def configure_form(self, f):
super(VendorCatalogView, self).configure_form(f)
app = self.get_rattail_app()
vendor_handler = app.get_vendor_handler()
# filename
f.set_label('filename', "Catalog File")
# parser_key
if self.creating:
if 'parser_key' not in f:
f.insert_after('filename', 'parser_key')
parsers = self.get_parsers()
values = [(p.key, p.display) for p in parsers]
if len(values) == 1:
f.set_default('parser_key', parsers[0].key)
f.set_widget('parser_key', dfwidget.SelectWidget(values=values))
else:
f.set_readonly('parser_key')
f.set_renderer('parser_key', self.render_parser_key)
# vendor
f.set_renderer('vendor', self.render_vendor)
if self.creating and 'vendor' in f:
f.replace('vendor', 'vendor_uuid')
f.set_label('vendor_uuid', "Vendor")
f.set_required('vendor_uuid')
f.set_validator('vendor_uuid', self.valid_vendor_uuid)
# should we use dropdown or autocomplete? note that if
# autocomplete is to be used, we also must make sure we
# have an autocomplete url registered
use_dropdown = vendor_handler.choice_uses_dropdown()
if not use_dropdown:
try:
vendors_url = self.request.route_url('vendors.autocomplete')
except KeyError:
use_dropdown = True
if use_dropdown:
vendors = self.Session.query(model.Vendor)\
.order_by(model.Vendor.id)
vendor_values = [(vendor.uuid, "({}) {}".format(vendor.id,
vendor.name))
for vendor in vendors]
f.set_widget('vendor_uuid',
dfwidget.SelectWidget(values=vendor_values))
else:
vendor_display = ""
if self.request.method == 'POST':
if self.request.POST.get('vendor_uuid'):
vendor = self.Session.get(model.Vendor,
self.request.POST['vendor_uuid'])
if vendor:
vendor_display = str(vendor)
f.set_widget('vendor_uuid',
forms.widgets.JQueryAutocompleteWidget(
field_display=vendor_display,
service_url=vendors_url,
ref='vendorAutocomplete',
assigned_label='vendorName',
input_callback='vendorChanged',
new_label_callback='vendorLabelChanging'))
else:
f.set_readonly('vendor')
if self.batch_handler.allow_future():
# effective
f.set_type('effective', 'date_jquery')
else: # future not allowed
f.remove('future',
'effective')
if self.creating:
f.set_node('cache_products', colander.Boolean())
f.set_type('cache_products', 'boolean')
f.set_helptext('cache_products',
"If set, will pre-cache all products for quicker "
"lookups when loading the catalog.")
else:
f.remove('cache_products')
def render_parser_key(self, batch, field):
key = getattr(batch, field)
if not key:
return
app = self.get_rattail_app()
vendor_handler = app.get_vendor_handler()
parser = vendor_handler.get_catalog_parser(key)
return parser.display
def template_kwargs_create(self, **kwargs):
app = self.get_rattail_app()
vendor_handler = app.get_vendor_handler()
parsers = self.get_parsers()
parsers_data = {}
for parser in parsers:
pdata = {'key': parser.key,
'vendor_key': parser.vendor_key}
if parser.vendor_key:
vendor = vendor_handler.get_vendor(self.Session(),
parser.vendor_key)
if vendor:
pdata['vendor_uuid'] = vendor.uuid
pdata['vendor_name'] = vendor.name
parsers_data[parser.key] = pdata
kwargs['parsers'] = parsers
kwargs['parsers_data'] = parsers_data
return kwargs
def get_batch_kwargs(self, batch):
kwargs = super(VendorCatalogView, self).get_batch_kwargs(batch)
kwargs['parser_key'] = batch.parser_key
if batch.vendor:
kwargs['vendor'] = batch.vendor
elif batch.vendor_uuid:
kwargs['vendor_uuid'] = batch.vendor_uuid
if batch.vendor_id:
kwargs['vendor_id'] = batch.vendor_id
if batch.vendor_name:
kwargs['vendor_name'] = batch.vendor_name
if self.batch_handler.allow_future():
kwargs['future'] = batch.future
kwargs['effective'] = batch.effective
return kwargs
def save_create_form(self, form):
batch = super(VendorCatalogView, self).save_create_form(form)
batch.set_param('cache_products', form.validated['cache_products'])
return batch
def configure_row_grid(self, g):
super(VendorCatalogView, self).configure_row_grid(g)
batch = self.get_instance()
# starts
if not batch.future:
g.remove('starts')
g.set_type('old_unit_cost', 'currency')
g.set_type('unit_cost', 'currency')
g.set_type('unit_cost_diff', 'currency')
g.set_type('unit_cost_diff_percent', 'percent')
g.set_label('unit_cost_diff_percent', "Diff. %")
g.set_label('is_preferred_vendor', "Pref. Vendor")
g.set_label('upc', "UPC")
g.set_label('brand_name', "Brand")
g.set_label('old_unit_cost', "Old Cost")
g.set_label('unit_cost', "New Cost")
g.set_label('unit_cost_diff', "Diff. $")
g.set_link('upc')
g.set_link('brand')
g.set_link('description')
g.set_link('vendor_code')
def row_grid_extra_class(self, row, i):
if row.status_code == row.STATUS_PRODUCT_NOT_FOUND:
return 'warning'
if row.status_code in (row.STATUS_NEW_COST,
row.STATUS_UPDATE_COST, # TODO: deprecate/remove this one
row.STATUS_CHANGE_VENDOR_ITEM_CODE,
row.STATUS_CHANGE_CASE_SIZE,
row.STATUS_CHANGE_COST,
row.STATUS_CHANGE_PRODUCT):
return 'notice'
def configure_row_form(self, f):
super(VendorCatalogView, self).configure_row_form(f)
f.set_renderer('product', self.render_product)
f.set_type('upc', 'gpc')
f.set_type('discount_percent', 'percent')
f.set_type('suggested_retail', 'currency')
def template_kwargs_view_row(self, **kwargs):
row = kwargs['instance']
batch = row.batch
fields = [
'vendor_code',
'case_size',
'case_cost',
'unit_cost',
]
old_data = dict([(field, getattr(row, 'old_{}'.format(field)))
for field in fields])
new_data = dict([(field, getattr(row, field))
for field in fields])
kwargs['catalog_entry_diff'] = Diff(old_data, new_data, fields=fields,
monospace=True)
return kwargs
def configure_get_simple_settings(self):
settings = super(VendorCatalogView, self).configure_get_simple_settings() or []
settings.extend([
# key field
{'section': 'rattail.batch',
'option': 'vendor_catalog.allow_future',
'type': bool},
])
return settings
def configure_get_context(self):
context = super(VendorCatalogView, self).configure_get_context()
app = self.get_rattail_app()
vendor_handler = app.get_vendor_handler()
Parsers = vendor_handler.get_all_catalog_parsers()
Supported = vendor_handler.get_supported_catalog_parsers()
context['catalog_parsers'] = Parsers
context['catalog_parsers_data'] = dict([(Parser.key, Parser in Supported)
for Parser in Parsers])
return context
def configure_gather_settings(self, data):
settings = super(VendorCatalogView, self).configure_gather_settings(data)
app = self.get_rattail_app()
vendor_handler = app.get_vendor_handler()
supported = []
for Parser in vendor_handler.get_all_catalog_parsers():
name = 'catalog_parser_{}'.format(Parser.key)
if data.get(name) == 'true':
supported.append(Parser.key)
settings.append({'name': 'rattail.vendors.supported_catalog_parsers',
'value': ', '.join(supported)})
return settings
def configure_remove_settings(self):
super(VendorCatalogView, self).configure_remove_settings()
app = self.get_rattail_app()
names = [
'rattail.vendors.supported_catalog_parsers',
'tailbone.batch.vendorcatalog.supported_parsers', # deprecated
]
# nb. using thread-local session here; we do not use
# self.Session b/c it may not point to Rattail
session = Session()
for name in names:
app.delete_setting(session, name)
# TODO: deprecate / remove this
VendorCatalogsView = VendorCatalogView
def defaults(config, **kwargs):
base = globals()
VendorCatalogView = kwargs.get('VendorCatalogView', base['VendorCatalogView'])
VendorCatalogView.defaults(config)
def includeme(config):
defaults(config)