diff --git a/rattail_corepos/db/alembic/versions/b5a8734b1fe0_fix_fk_for_coreproductcost.py b/rattail_corepos/db/alembic/versions/b5a8734b1fe0_fix_fk_for_coreproductcost.py new file mode 100644 index 0000000..138ac75 --- /dev/null +++ b/rattail_corepos/db/alembic/versions/b5a8734b1fe0_fix_fk_for_coreproductcost.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8; -*- +"""fix FK for CoreProductCost + +Revision ID: b5a8734b1fe0 +Revises: 15bf65f68c52 +Create Date: 2023-10-20 11:20:09.231682 + +""" + +# revision identifiers, used by Alembic. +revision = 'b5a8734b1fe0' +down_revision = '15bf65f68c52' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa +import rattail.db.types + + + +def upgrade(): + + # corepos_product_cost + op.alter_column('corepos_product_cost', 'corepos_id', existing_type=sa.INTEGER(), nullable=True) + op.add_column('corepos_product_cost', sa.Column('corepos_vendor_id', sa.Integer(), nullable=False)) + op.add_column('corepos_product_cost', sa.Column('corepos_sku', sa.String(length=13), nullable=False)) + op.add_column('corepos_product_cost_version', sa.Column('corepos_vendor_id', sa.Integer(), autoincrement=False, nullable=True)) + op.add_column('corepos_product_cost_version', sa.Column('corepos_sku', sa.String(length=13), autoincrement=False, nullable=True)) + + +def downgrade(): + + # corepos_product_cost + op.drop_column('corepos_product_cost_version', 'corepos_sku') + op.drop_column('corepos_product_cost_version', 'corepos_vendor_id') + op.drop_column('corepos_product_cost', 'corepos_sku') + op.drop_column('corepos_product_cost', 'corepos_vendor_id') + op.alter_column('corepos_product_cost', 'corepos_id', existing_type=sa.INTEGER(), nullable=False) diff --git a/rattail_corepos/db/model/products.py b/rattail_corepos/db/model/products.py index 0398dc4..180a972 100644 --- a/rattail_corepos/db/model/products.py +++ b/rattail_corepos/db/model/products.py @@ -195,11 +195,24 @@ class CoreProductCost(model.Base): 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. + corepos_vendor_id = sa.Column(sa.Integer(), nullable=False, doc=""" + ``vendorItems.vendorID`` value for the corresponding record within + CORE-POS. + """) + + corepos_sku = sa.Column(sa.String(length=13), nullable=False, doc=""" + ``vendorItems.sku`` value for the corresponding record within + CORE-POS. + """) + + corepos_id = sa.Column(sa.Integer(), nullable=True, doc=""" + ``vendorItems.vendorItemID`` value for the corresponding record + within CORE-POS. """) def __str__(self): return str(self.cost) +CoreProductCost.make_proxy(model.ProductCost, '_corepos', 'corepos_vendor_id') +CoreProductCost.make_proxy(model.ProductCost, '_corepos', 'corepos_sku') 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 3b1ff6f..ed94bc0 100644 --- a/rattail_corepos/importing/corepos/api.py +++ b/rattail_corepos/importing/corepos/api.py @@ -646,9 +646,12 @@ class ProductCostImporter(FromCOREPOSAPI, corepos_importing.model.ProductCostImp """ Importer for product cost data from CORE POS API. """ + # TODO: should change key after live sites are updated key = ('vendor_uuid', 'code') + # key = ('corepos_vendor_id', 'corepos_sku') supported_fields = [ - 'corepos_id', + 'corepos_vendor_id', + 'corepos_sku', 'product_uuid', 'vendor_uuid', 'code', @@ -752,7 +755,8 @@ class ProductCostImporter(FromCOREPOSAPI, corepos_importing.model.ProductCostImp case_cost = unit_cost * case_size return { - 'corepos_id': int(item['vendorItemID']), + 'corepos_vendor_id': int(item['vendorID']), + 'corepos_sku': item['sku'], 'product_uuid': product.uuid, 'vendor_uuid': vendor.uuid, 'code': (item['sku'] or '').strip() or None, diff --git a/rattail_corepos/importing/corepos/db.py b/rattail_corepos/importing/corepos/db.py index 47f71b6..6017fa7 100644 --- a/rattail_corepos/importing/corepos/db.py +++ b/rattail_corepos/importing/corepos/db.py @@ -68,6 +68,7 @@ class FromCOREPOSToRattail(importing.FromSQLAlchemyHandler, importing.ToRattailH def get_importers(self): importers = OrderedDict() + importers['Store'] = StoreImporter importers['Employee'] = EmployeeImporter importers['Customer'] = CustomerImporter importers['Member'] = MemberImporter @@ -77,6 +78,7 @@ class FromCOREPOSToRattail(importing.FromSQLAlchemyHandler, importing.ToRattailH importers['Department'] = DepartmentImporter importers['Subdepartment'] = SubdepartmentImporter importers['Product'] = ProductImporter + importers['ProductCost'] = ProductCostImporter importers['MemberEquityPayment'] = MemberEquityPaymentImporter return importers @@ -114,6 +116,26 @@ class FromCOREPOS(importing.FromSQLAlchemy): return False +class StoreImporter(FromCOREPOS, corepos_importing.model.StoreImporter): + """ + Importer for store data from CORE POS. + """ + host_model_class = corepos.Store + key = 'corepos_id' + supported_fields = [ + 'corepos_id', + 'id', + 'name', + ] + + def normalize_host_object(self, store): + return { + 'corepos_id': store.id, + 'id': str(store.id), + 'name': store.description, + } + + class EmployeeImporter(FromCOREPOS, corepos_importing.model.EmployeeImporter): """ Importer for employee data from CORE POS. @@ -445,7 +467,7 @@ class VendorImporter(FromCOREPOS, corepos_importing.model.VendorImporter): model = self.config.get_model() # first get default query - query = super(VendorImporter, self).cache_query() + query = super().cache_query() # maybe filter a bit, to ensure only "relevant" records are involved if 'corepos_id' in self.key: @@ -546,6 +568,7 @@ class ProductImporter(FromCOREPOS, corepos_importing.model.ProductImporter): 'sale_price_ends', 'sale_price_current', 'food_stampable', + 'discountable', # 'tax1', 'tax_code', 'not_for_sale', @@ -629,6 +652,8 @@ class ProductImporter(FromCOREPOS, corepos_importing.model.ProductImporter): 'sale_price_ends': None, 'sale_price_current': False, + 'discountable': bool(product.line_item_discountable), + 'not_for_sale': not product.in_use, } @@ -659,6 +684,113 @@ class ProductImporter(FromCOREPOS, corepos_importing.model.ProductImporter): return data +class ProductCostImporter(FromCOREPOS, corepos_importing.model.ProductCostImporter): + """ + Importer for product cost data from CORE POS API. + """ + host_model_class = corepos.VendorItem + key = ('corepos_vendor_id', 'corepos_sku') + supported_fields = [ + 'corepos_vendor_id', + 'corepos_sku', + 'product_uuid', + 'vendor_uuid', + 'code', + 'case_size', + 'case_cost', + 'unit_cost', + 'preferred', + ] + + def setup(self): + super().setup() + model = self.model + + query = self.session.query(model.Vendor)\ + .join(model.CoreVendor)\ + .filter(model.CoreVendor.corepos_id != None) + self.vendors_by_corepos_id = self.cache_model(model.Vendor, + query=query, + key='corepos_id') + + self.products_by_item_id = self.cache_model(model.Product, key='item_id') + + def query(self): + query = super().query() + + query = query.options(orm.joinedload(corepos.VendorItem.product)) + + return query + + def get_vendor(self, item): + corepos_id = item.vendor_id + + if hasattr(self, 'vendors_by_corepos_id'): + return self.vendors_by_corepos_id.get(corepos_id) + + model = self.config.get_model() + 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_product(self, item): + item_id = item.upc + + if hasattr(self, 'products_by_item_id'): + return self.products_by_item_id.get(item_id) + + model = self.model + try: + return self.session.query(model.Product)\ + .filter(model.Product.item_id == item_id)\ + .one() + except orm.exc.NoResultFound: + pass + + def normalize_host_object(self, item): + + vendor = self.get_vendor(item) + if not vendor: + log.warning("CORE POS vendor not found for item: %s", item) + return + + product = self.get_product(item) + if not product: + # just debug logging since this is a common scenario; the + # CORE table is for items "available from vendor" but not + # necssarily items carried by store + log.debug("product not found for CORE vendor item: %s", item) + return + + core_product = item.product + + preferred = False + if core_product and core_product.default_vendor_id == item.vendor_id: + preferred = True + + case_size = decimal.Decimal(str(item.units)) + unit_cost = item.cost + case_cost = None + if unit_cost is not None: + case_cost = unit_cost * case_size + + return { + 'corepos_vendor_id': item.vendor_id, + 'corepos_sku': item.sku, + 'product_uuid': product.uuid, + 'vendor_uuid': vendor.uuid, + 'code': item.sku, + 'case_size': case_size, + 'case_cost': case_cost, + 'unit_cost': unit_cost, + 'preferred': preferred, + } + + class MemberEquityPaymentImporter(FromCOREPOS, corepos_importing.model.MemberEquityPaymentImporter): """ Imports equity payment data from CORE-POS diff --git a/rattail_corepos/importing/model.py b/rattail_corepos/importing/model.py index e484f5c..bf301e0 100644 --- a/rattail_corepos/importing/model.py +++ b/rattail_corepos/importing/model.py @@ -43,7 +43,7 @@ class PersonImporter(importing.model.PersonImporter): } def cache_query(self): - query = super(PersonImporter, self).cache_query() + query = super().cache_query() model = self.config.get_model() # we want to ignore people with no CORE ID, if that's (part of) our key @@ -168,7 +168,7 @@ class ProductImporter(importing.model.ProductImporter): } def setup(self): - super(ProductImporter, self).setup() + super().setup() if self.fields_active(self.size_fields): app = self.config.get_app() @@ -176,7 +176,7 @@ class ProductImporter(importing.model.ProductImporter): self.uoms = handler.get_uom_sil_codes(self.session, uppercase=True) def cache_query(self): - query = super(ProductImporter, self).cache_query() + query = super().cache_query() model = self.config.get_model() # we want to ignore products with no CORE ID, if that's (part of) our key @@ -239,12 +239,14 @@ class ProductCostImporter(importing.model.ProductCostImporter): extensions = { '_corepos': [ + 'corepos_vendor_id', + 'corepos_sku', 'corepos_id', ], } def cache_query(self): - query = super(ProductCostImporter, self).cache_query() + query = super().cache_query() model = self.config.get_model() # we want to ignore items with no CORE ID, if that's (part of) our key