b4816c6289
and add validation for new Ordering batch
446 lines
15 KiB
Python
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)
|