Add basic support for vendor catalog batch feature

This commit is contained in:
Lance Edgar 2021-02-15 13:11:47 -06:00
parent b8ce21b6c0
commit 1177ca6591
9 changed files with 301 additions and 16 deletions

View file

View file

@ -0,0 +1,208 @@
# -*- coding: utf-8; -*-
"""
Handler for Vendor Catalog batches
"""
import decimal
from sqlalchemy import orm
from corepos.db.office_op import Session as CoreSession, model as corepos
from rattail_corepos.corepos.api import make_corepos_api
from rattail_corepos.batch import vendorcatalog as base
class VendorCatalogHandler(base.VendorCatalogHandler):
"""
Handler for vendor catalog batches.
"""
# upstream logic requires versioning hacks, but we do not
populate_with_versioning = True
refresh_with_versioning = True
execute_with_versioning = True
# catalog parsers will favor case cost, but CORE favors unit cost. so we
# always "ignore" unit cost diffs less than one penny to avoid useless
# updates caused by simple rounding
unit_cost_diff_threshold = decimal.Decimal('0.01')
def setup_common(self, batch, progress=None):
self.core_session = CoreSession()
query = self.core_session.query(corepos.Product)\
.options(orm.joinedload(corepos.Product.vendor_items))
self.core_products_by_upc = self.cache_model(self.core_session,
corepos.Product,
key='upc',
query=query)
query = self.core_session.query(corepos.VendorItem)\
.filter(corepos.VendorItem.vendor_id == int(batch.vendor_id))
self.core_vendor_items_by_sku = self.cache_model(self.core_session,
corepos.VendorItem,
key='sku',
query=query)
setup_populate = setup_common
setup_refresh = setup_common
def teardown_common(self, batch, progress=None):
self.core_session.rollback()
self.core_session.close()
del self.core_session
teardown_populate = teardown_common
teardown_refresh = teardown_common
def add_row(self, batch, row):
# parser logic sets upc but we want to use item_id
row.item_id = str(row.upc)[:-1]
# okay now continue as normal
# (note, this must come last b/c it will refresh row)
super(VendorCatalogHandler, self).add_row(batch, row)
def refresh_row(self, row):
# clear this first in case it's set
row.status_text = None
# find the CORE `products` record, matching by `products.upc`
core_product = self.core_products_by_upc.get(row.item_id)
# find the CORE `vendorItems` record, matching by `vendorItems.sku`
core_vendor_item = self.core_vendor_items_by_sku.get(row.vendor_code)
# if the catalog UPC is not found in `products` but the SKU *is* found
# in `vendorItems` *and* the latter ties back to valid `products`
# record, then we want to pretend that matched all along. and not just
# for the moment, but going forward; so we also update `row.item_id`
if not core_product and core_vendor_item:
core_product = core_vendor_item.product
if core_product:
row.item_id = core_product.upc
# figure out if this vendor is already default for the product. if the
# product does not yet have a default, let this vendor be it.
row.is_preferred_vendor = False
row.make_preferred_vendor = False
if core_product:
if core_product.default_vendor_id and (
str(core_product.default_vendor_id) == row.batch.vendor_id):
row.is_preferred_vendor = True
if not core_product.default_vendor_id:
row.make_preferred_vendor = True
# declare "product not found" if we did not find any matches in CORE
if not core_vendor_item and not core_product:
row.status_code = row.STATUS_PRODUCT_NOT_FOUND
return
# declare "new cost" if we found `products` match but not `vendorItems`
if not core_vendor_item:
row.status_code = row.STATUS_NEW_COST
return
# declare "change product" if `vendorItems.upc` != `products.upc`
if core_vendor_item.upc != row.item_id:
row.status_code = row.STATUS_CHANGE_PRODUCT
row.status_text = "new UPC {} differs from old UPC {}".format(
row.item_id, core_vendor_item.upc)
return
# declare "old" `vendorItems` data
row.old_vendor_code = core_vendor_item.sku
row.old_case_size = core_vendor_item.units
row.old_unit_cost = core_vendor_item.cost
row.old_case_cost = (core_vendor_item.cost * decimal.Decimal(core_vendor_item.units))\
.quantize(decimal.Decimal('0.12345'))
self.refresh_cost_diffs(row)
self.set_status_per_diffs(row)
def describe_execution(self, batch, **kwargs):
return ("The `vendorItems` table in CORE-POS will be updated directly "
"via API, for all rows indicating a change etc. In some cases "
"`products` may also be updated as appropriate.")
def execute(self, batch, progress=None, **kwargs):
"""
Update CORE-POS etc.
"""
rows = [row for row in batch.active_rows()
if row.status_code in (row.STATUS_NEW_COST,
# row.STATUS_UPDATE_COST,
# row.STATUS_CHANGE_VENDOR_ITEM_CODE,
row.STATUS_CHANGE_CASE_SIZE,
row.STATUS_CHANGE_COST,
row.STATUS_CHANGE_PRODUCT)]
if rows:
self.api = make_corepos_api(self.config)
self.update_corepos(batch, rows, batch.vendor_id, progress=progress,
# update_product_costs=kwargs.get('update_product_costs', False),
)
return True
def update_corepos(self, batch, rows, vendor_id, progress=None,
# TODO: this kwarg seems perhaps useful, but for now we
# are auto-detecting when such an update is needed
#update_product_costs=False,
):
"""
Update the `vendorItems` table in CORE-POS (and maybe `products` too).
"""
def update(row, i):
# we may need this value in a couple places
unit_cost = float(row.unit_cost)
# figure out if we are "updating the same, primary" cost record,
# b/c if so we will want to update product accordingly also. this
# is always the case when this is the first vendor for product.
# updating_primary = first_vendor
updating_primary = row.make_preferred_vendor
if not updating_primary:
core_vendor_items = self.api.get_vendor_items(upc=row.item_id)
if core_vendor_items:
core_vendor_item = core_vendor_items[0]
if core_vendor_item['sku'] == row.vendor_code:
updating_primary = True
# create or update the `vendorItems` record in CORE
self.api.set_vendor_item(sku=row.vendor_code,
vendorID=vendor_id,
upc=row.item_id,
brand=row.brand_name,
description=row.description,
size=row.size,
units=row.case_size,
cost=unit_cost,
# TODO: we (may) have vendor SRP, but pretty
# sure CORE has different plans for its `srp`
#srp=row.suggested_retail,
)
# TODO: CORE does not have the concept of a true "default"
# `vendorItems` record, but rather uses the `modified` timestamp
# for pseudo-default. this means any given product may wind up
# with a new/different pseudo-default when the above operation
# completes. in which case, perhaps we should *always* update
# `products.cost` accordingly (below)..? er, still only if the
# product's `default_vendor_id` matches at least, i guess... for
# now we are only doing so if it "obviously" needs it.
# maybe also update `products` record in CORE
kwargs = {}
if updating_primary:
kwargs['cost'] = unit_cost
if row.make_preferred_vendor:
kwargs['default_vendor_id'] = vendor_id
if kwargs:
self.api.set_product(upc=row.item_id, **kwargs)
self.progress_loop(update, rows, progress,
message="Updating CORE-POS via API")

View file

@ -14,7 +14,11 @@ class CorporalConfig(ConfigExtension):
def configure(self, config):
# set some default config values
config.setdefault('rattail', 'model', 'corporal.db.model')
config.setdefault('rattail.mail', 'emails', 'corporal.emails')
config.setdefault('rattail', 'settings', 'corporal.settings')
config.setdefault('tailbone', 'menus', 'corporal.web.menus')
config.setdefault('rattail.config', 'templates', 'corporal:data/config rattail:data/config')
# batches
config.setdefault('rattail.batch', 'vendor_catalog.handler', 'corporal.batch.vendorcatalog:VendorCatalogHandler')

0
corporal/db/__init__.py Normal file
View file

10
corporal/db/model.py Normal file
View file

@ -0,0 +1,10 @@
# -*- coding: utf-8; -*-
"""
Corporal data model w/ CORE-POS integration
"""
# bring in all the normal stuff from Rattail
from rattail.db.model import *
# also bring in CORE integration models
from rattail_corepos.db.model import *

View file

@ -11,6 +11,18 @@ def simple_menus(request):
corepos_menu = make_corepos_menu(request)
batch_menu = {
'title': "Batches",
'type': 'menu',
'items': [
{
'title': "Vendor Catalogs",
'url': url('vendorcatalogs'),
'perm': 'vendorcatalogs.list',
},
],
}
admin_menu = {
'title': "Admin",
'type': 'menu',
@ -67,6 +79,7 @@ def simple_menus(request):
menus = [
corepos_menu,
batch_menu,
admin_menu,
]

View file

@ -14,27 +14,14 @@ def includeme(config):
config.include('tailbone.views.progress')
# main table views
config.include('tailbone.views.customergroups')
config.include('tailbone.views.datasync')
config.include('tailbone.views.email')
config.include('tailbone.views.families')
config.include('tailbone_corepos.views.members')
config.include('tailbone.views.messages')
config.include('tailbone_corepos.views.people')
config.include('tailbone.views.reportcodes')
config.include('tailbone.views.roles')
config.include('tailbone.views.settings')
config.include('tailbone_corepos.views.subdepartments')
config.include('tailbone.views.shifts')
config.include('tailbone.views.users')
config.include('tailbone.views.stores')
config.include('tailbone_corepos.views.customers')
config.include('tailbone.views.employees')
config.include('tailbone.views.taxes')
config.include('tailbone_corepos.views.departments')
config.include('tailbone.views.brands')
config.include('tailbone_corepos.views.vendors')
config.include('tailbone_corepos.views.products')
# CORE-POS direct data views
config.include('tailbone_corepos.views.corepos')
# batches
config.include('corporal.web.views.batch.vendorcatalog')

View file

View file

@ -0,0 +1,63 @@
# -*- coding: utf-8; -*-
"""
Vendor Catalog Batch views for Corporal
"""
from corepos.db.office_op import model as corepos
from deform import widget as dfwidget
from tailbone.views.batch import vendorcatalog as base
from tailbone_corepos.db import CoreOfficeSession
class VendorCatalogView(base.VendorCatalogView):
"""
Master view for vendor catalog batches.
"""
form_fields = [
'id',
'description',
'vendor_id',
'vendor_name',
'filename',
'notes',
'created',
'created_by',
'rowcount',
'executed',
'executed_by',
]
def configure_form(self, f):
super(VendorCatalogView, self).configure_form(f)
model = self.model
# vendor_id
if self.creating:
vendors = CoreOfficeSession.query(corepos.Vendor)\
.order_by(corepos.Vendor.name)\
.all()
values = [(str(vendor.id), vendor.name)
for vendor in vendors]
f.set_widget('vendor_id', dfwidget.SelectWidget(values=values))
f.set_required('vendor_id')
f.set_label('vendor_id', "Vendor")
# vendor_name
if self.creating:
f.remove('vendor_name')
def get_batch_kwargs(self, batch):
kwargs = super(VendorCatalogView, self).get_batch_kwargs(batch)
if 'vendor_name' not in kwargs and batch.vendor_id:
vendor = CoreOfficeSession.query(corepos.Vendor).get(batch.vendor_id)
if vendor:
kwargs['vendor_name'] = vendor.name
return kwargs
def includeme(config):
VendorCatalogView.defaults(config)