From d0bb4b105d9590967398c3f7cad65dc11c09c671 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 9 Feb 2021 15:31:43 -0600 Subject: [PATCH] Add custom vendor catalog batch handler for CORE capable of directly updating `vendorItems` etc. via CORE API --- rattail_corepos/batch/__init__.py | 0 rattail_corepos/batch/vendorcatalog.py | 149 +++++++++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 rattail_corepos/batch/__init__.py create mode 100644 rattail_corepos/batch/vendorcatalog.py diff --git a/rattail_corepos/batch/__init__.py b/rattail_corepos/batch/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rattail_corepos/batch/vendorcatalog.py b/rattail_corepos/batch/vendorcatalog.py new file mode 100644 index 0000000..ff6cb00 --- /dev/null +++ b/rattail_corepos/batch/vendorcatalog.py @@ -0,0 +1,149 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2021 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 . +# +################################################################################ +""" +Handler for Vendor Catalog batches +""" + +import decimal + +from rattail.batch import vendorcatalog as base +from rattail_corepos.corepos.api import make_corepos_api + + +class VendorCatalogHandler(base.VendorCatalogHandler): + """ + Handler for vendor catalog batches. + """ + # default logic tries to update versions after execution, but since we just + # update CORE-POS via API there's no need + version_catchup_execute = None + + def add_row(self, batch, row): + + # CORE unit costs seem to be rounded to 3 decimal places + if row.unit_cost: + row.unit_cost = row.unit_cost.quantize(decimal.Decimal('0.123')) + + # CORE bases its cost off the unit instead of case. so here we make + # sure the case cost is cleanly derived from unit cost. + if row.case_cost and row.unit_cost and row.case_size: + row.case_cost = row.unit_cost * row.case_size + + # okay now continue as normal + super(VendorCatalogHandler, self).add_row(batch, 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. You may also " + "update `products.cost` if desired.") + + 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)] + + if rows: + vendor_id = batch.vendor.corepos_id + if not vendor_id: + raise ValueError("Batch vendor does not have valid CORE-POS ID") + + self.api = make_corepos_api(self.config) + self.update_corepos(batch, rows, 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) + + # we will want to set default vendor for the product, if it does + # not yet have one + first_vendor = False + if not row.product.costs: + first_vendor = True + # core_product = self.api.get_product(row.item_id) + # if not core_product['default_vendor_id']: + # first_vendor = True + + # 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 + if not updating_primary: + cost = row.product.cost + if cost and cost is row.cost: + updating_primary = True + # core_items = self.api.get_vendor_items(upc=row.item_id) + # if core_items: + # core_item = core_items[0] + # if core_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 + if updating_primary: + kwargs = {'cost': unit_cost} + if first_vendor: + kwargs['default_vendor_id'] = vendor_id + self.api.set_product(upc=row.item_id, **kwargs) + + self.progress_loop(update, rows, progress, + message="Updating CORE-POS via API")