Add basic support for vendor catalog batch feature
This commit is contained in:
parent
b8ce21b6c0
commit
1177ca6591
0
corporal/batch/__init__.py
Normal file
0
corporal/batch/__init__.py
Normal file
208
corporal/batch/vendorcatalog.py
Normal file
208
corporal/batch/vendorcatalog.py
Normal 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")
|
|
@ -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
0
corporal/db/__init__.py
Normal file
10
corporal/db/model.py
Normal file
10
corporal/db/model.py
Normal 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 *
|
|
@ -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,
|
||||
]
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
0
corporal/web/views/batch/__init__.py
Normal file
0
corporal/web/views/batch/__init__.py
Normal file
63
corporal/web/views/batch/vendorcatalog.py
Normal file
63
corporal/web/views/batch/vendorcatalog.py
Normal 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)
|
Loading…
Reference in a new issue