From 580f2093ae4e1f1dfe2137b50f830f926b340303 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 4 Sep 2020 19:10:48 -0500 Subject: [PATCH] Add schema, logic for importing CORE VendorItem -> ProductCost --- .../130e4632a28a_add_corepos_product_cost.py | 51 ++++++++++ rattail_corepos/db/model/__init__.py | 2 +- rattail_corepos/db/model/products.py | 35 +++++++ rattail_corepos/importing/corepos/api.py | 94 +++++++++++++++++++ rattail_corepos/importing/model.py | 19 ++++ 5 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 rattail_corepos/db/alembic/versions/130e4632a28a_add_corepos_product_cost.py diff --git a/rattail_corepos/db/alembic/versions/130e4632a28a_add_corepos_product_cost.py b/rattail_corepos/db/alembic/versions/130e4632a28a_add_corepos_product_cost.py new file mode 100644 index 0000000..ef7ced2 --- /dev/null +++ b/rattail_corepos/db/alembic/versions/130e4632a28a_add_corepos_product_cost.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +"""add corepos_product_cost + +Revision ID: 130e4632a28a +Revises: 9c5029effe93 +Create Date: 2020-09-04 18:30:17.041521 + +""" + +# revision identifiers, used by Alembic. +revision = '130e4632a28a' +down_revision = '9c5029effe93' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa +import rattail.db.types + + + +def upgrade(): + + # corepos_product_cost + op.create_table('corepos_product_cost', + sa.Column('uuid', sa.String(length=32), nullable=False), + sa.Column('corepos_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['uuid'], ['product_cost.uuid'], name='corepos_product_cost_fk_cost'), + sa.PrimaryKeyConstraint('uuid') + ) + op.create_table('corepos_product_cost_version', + sa.Column('uuid', sa.String(length=32), autoincrement=False, nullable=False), + sa.Column('corepos_id', sa.Integer(), autoincrement=False, nullable=True), + sa.Column('transaction_id', sa.BigInteger(), autoincrement=False, nullable=False), + sa.Column('end_transaction_id', sa.BigInteger(), nullable=True), + sa.Column('operation_type', sa.SmallInteger(), nullable=False), + sa.PrimaryKeyConstraint('uuid', 'transaction_id') + ) + op.create_index(op.f('ix_corepos_product_cost_version_end_transaction_id'), 'corepos_product_cost_version', ['end_transaction_id'], unique=False) + op.create_index(op.f('ix_corepos_product_cost_version_operation_type'), 'corepos_product_cost_version', ['operation_type'], unique=False) + op.create_index(op.f('ix_corepos_product_cost_version_transaction_id'), 'corepos_product_cost_version', ['transaction_id'], unique=False) + + +def downgrade(): + + # corepos_product_cost + op.drop_index(op.f('ix_corepos_product_cost_version_transaction_id'), table_name='corepos_product_cost_version') + op.drop_index(op.f('ix_corepos_product_cost_version_operation_type'), table_name='corepos_product_cost_version') + op.drop_index(op.f('ix_corepos_product_cost_version_end_transaction_id'), table_name='corepos_product_cost_version') + op.drop_table('corepos_product_cost_version') + op.drop_table('corepos_product_cost') diff --git a/rattail_corepos/db/model/__init__.py b/rattail_corepos/db/model/__init__.py index e21080a..2ac93f3 100644 --- a/rattail_corepos/db/model/__init__.py +++ b/rattail_corepos/db/model/__init__.py @@ -26,4 +26,4 @@ Database schema extensions for CORE-POS integration from .people import CorePerson, CoreCustomer, CoreMember from .products import (CoreDepartment, CoreSubdepartment, - CoreVendor, CoreProduct) + CoreVendor, CoreProduct, CoreProductCost) diff --git a/rattail_corepos/db/model/products.py b/rattail_corepos/db/model/products.py index ad664f6..0398dc4 100644 --- a/rattail_corepos/db/model/products.py +++ b/rattail_corepos/db/model/products.py @@ -168,3 +168,38 @@ class CoreProduct(model.Base): return str(self.product) CoreProduct.make_proxy(model.Product, '_corepos', 'corepos_id') + + +class CoreProductCost(model.Base): + """ + CORE-specific extensions to :class:`rattail:rattail.db.model.ProductCost`. + """ + __tablename__ = 'corepos_product_cost' + __table_args__ = ( + sa.ForeignKeyConstraint(['uuid'], ['product_cost.uuid'], + name='corepos_product_cost_fk_cost'), + ) + __versioned__ = {} + + uuid = model.uuid_column(default=None) + cost = orm.relationship( + model.ProductCost, + doc=""" + Reference to the actual ProductCost record, which this one extends. + """, + backref=orm.backref( + '_corepos', + uselist=False, + cascade='all, delete-orphan', + doc=""" + Reference to the CORE-POS extension record for this product cost. + """)) + + corepos_id = sa.Column(sa.Integer(), nullable=False, doc=""" + ``vendorItemID`` value for the corresponding record within CORE-POS. + """) + + def __str__(self): + return str(self.cost) + +CoreProductCost.make_proxy(model.ProductCost, '_corepos', 'corepos_id') diff --git a/rattail_corepos/importing/corepos/api.py b/rattail_corepos/importing/corepos/api.py index da2d2be..6cb8c39 100644 --- a/rattail_corepos/importing/corepos/api.py +++ b/rattail_corepos/importing/corepos/api.py @@ -61,6 +61,7 @@ class FromCOREPOSToRattail(importing.ToRattailHandler): importers['Subdepartment'] = SubdepartmentImporter importers['Vendor'] = VendorImporter importers['Product'] = ProductImporter + importers['ProductCost'] = ProductCostImporter importers['ProductMovement'] = ProductMovementImporter return importers @@ -570,6 +571,99 @@ class ProductMovementImporter(FromCOREPOSAPI, corepos_importing.model.ProductImp } +class ProductCostImporter(FromCOREPOSAPI, corepos_importing.model.ProductCostImporter): + """ + Importer for product cost data from CORE POS API. + """ + key = 'corepos_id' + supported_fields = [ + 'corepos_id', + 'product_upc', + 'vendor_uuid', + 'code', + 'case_size', + 'case_cost', + 'unit_cost', + 'preferred', + ] + + def setup(self): + super(ProductCostImporter, self).setup() + model = self.config.get_model() + + query = self.session.query(model.Vendor)\ + .join(model.CoreVendor)\ + .filter(model.CoreVendor.corepos_id != None) + self.vendors = self.cache_model(model.Vendor, query=query, + key='corepos_id') + + self.corepos_products = {} + + def cache(product, i): + self.corepos_products[product['upc']] = product + + self.progress_loop(cache, self.api.get_products(), + message="Caching Products from CORE-POS API") + + def get_host_objects(self): + return self.api.get_vendor_items() + + def get_vendor(self, item): + corepos_id = int(item['vendorID']) + + if hasattr(self, 'vendors'): + return self.vendors.get(corepos_id) + + try: + return self.session.query(model.Vendor)\ + .join(model.CoreVendor)\ + .filter(model.CoreVendor.corepos_id == corepos_id)\ + .one() + except orm.exc.NoResultFound: + pass + + def get_corepos_product(self, item): + if hasattr(self, 'corepos_products'): + return self.corepos_products.get(item['upc']) + + return self.api.get_vendor_item(item['upc'], item['vendorID']) + + def normalize_host_object(self, item): + try: + upc = GPC(item['upc'], calc_check_digit='upc') + except (TypeError, ValueError): + log.warning("CORE POS vendor item has invalid UPC: %s", item['upc']) + return + + vendor = self.get_vendor(item) + if not vendor: + log.warning("CORE POS vendor not found for item: %s", item) + return + + product = self.get_corepos_product(item) + # if not product: + # log.warning("CORE POS product not found for item: %s", item) + # return + + preferred = False + if product and product['default_vendor_id'] == item['vendorID']: + preferred = True + + case_size = decimal.Decimal(item['units']) + unit_cost = decimal.Decimal(item['cost']) + + return { + 'corepos_id': int(item['vendorItemID']), + 'product_upc': upc, + 'vendor_uuid': vendor.uuid, + 'code': (item['sku'] or '').strip() or None, + 'case_size': case_size, + 'case_cost': case_size * unit_cost, + 'unit_cost': unit_cost, + 'preferred': preferred, + } + + class MemberImporter(FromCOREPOSAPI, corepos_importing.model.MemberImporter): """ Importer for member data from CORE POS API. diff --git a/rattail_corepos/importing/model.py b/rattail_corepos/importing/model.py index 96c8d2d..1b8ea03 100644 --- a/rattail_corepos/importing/model.py +++ b/rattail_corepos/importing/model.py @@ -107,3 +107,22 @@ class ProductImporter(importing.model.ProductImporter): .filter(model.CoreProduct.corepos_id != None) return query + + +class ProductCostImporter(importing.model.ProductCostImporter): + + extension_attr = '_corepos' + extension_fields = [ + 'corepos_id', + ] + + def cache_query(self): + query = super(ProductCostImporter, self).cache_query() + model = self.config.get_model() + + # we want to ignore items with no CORE ID, if that's (part of) our key + if 'corepos_id' in self.key: + query = query.join(model.CoreProductCost)\ + .filter(model.CoreProductCost.corepos_id != None) + + return query