diff --git a/rattail_corepos/corepos/importing/model.py b/rattail_corepos/corepos/importing/model.py index 84ab883..2199ef9 100644 --- a/rattail_corepos/corepos/importing/model.py +++ b/rattail_corepos/corepos/importing/model.py @@ -33,6 +33,11 @@ class ToCoreAPI(importing.Importer): """ Base class for all CORE "operational" model importers, which use the API. """ + # TODO: these importers are in a bit of an experimental state at the + # moment. we only allow create/update b/c it will use the API instead of + # direct DB + allow_delete = False + caches_local_data = True def setup(self): @@ -69,6 +74,98 @@ class ToCoreAPI(importing.Importer): data[field] = '' +class DepartmentImporter(ToCoreAPI): + """ + Department model importer for CORE-POS + """ + model_name = 'Department' + key = 'dept_no' + supported_fields = [ + 'dept_no', + 'dept_name', + # TODO: should enable some of these fields? + # 'dept_tax', + # 'dept_fs', + # 'dept_limit', + # 'dept_minimum', + # 'dept_discount', + # 'dept_see_id', + # 'modified', + # 'modifiedby', + # 'margin', + # 'salesCode', + # 'memberOnly', + ] + + def get_local_objects(self, host_data=None): + return self.api.get_departments() + + def get_single_local_object(self, key): + assert len(self.key) == 1 + assert self.key[0] == 'dept_no' + return self.api.get_department(key[0]) + + def normalize_local_object(self, department): + data = dict(department) + return data + + def create_object(self, key, data): + # we can get away with using the same logic for both here + return self.update_object(None, data) + + def update_object(self, department, data, local_data=None): + """ + Push an update for the department, via the CORE API. + """ + if self.dry_run: + return data + + dept_no = data.pop('dept_no') + department = self.api.set_department(dept_no, **data) + return department + + +class SubdepartmentImporter(ToCoreAPI): + """ + Subdepartment model importer for CORE-POS + """ + model_name = 'Subdepartment' + key = 'subdept_no' + supported_fields = [ + 'subdept_no', + 'subdept_name', + 'dept_ID', + ] + + def get_local_objects(self, host_data=None): + return self.api.get_subdepartments() + + def get_single_local_object(self, key): + assert len(self.key) == 1 + assert self.key[0] == 'subdept_no' + return self.api.get_subdepartment(key[0]) + + def normalize_local_object(self, subdepartment): + data = dict(subdepartment) + self.ensure_fields(data) + return data + + def create_object(self, key, data): + # we can get away with using the same logic for both here + return self.update_object(None, data) + + def update_object(self, subdepartment, data, local_data=None): + """ + Push an update for the subdepartment, via the CORE API. + """ + if self.dry_run: + return data + + subdept_no = data.pop('subdept_no') + subdepartment = self.api.set_subdepartment(subdept_no, **data) + return subdepartment + + class VendorImporter(ToCoreAPI): """ Vendor model importer for CORE-POS @@ -95,9 +192,6 @@ class VendorImporter(ToCoreAPI): 'orderMinimum', 'halfCases', ] - # TODO: this importer is in a bit of an experimental state at the moment. - # we only allow create/update b/c it will use the API instead of direct DB - allow_delete = False def get_local_objects(self, host_data=None): return self.api.get_vendors() @@ -135,3 +229,88 @@ class VendorImporter(ToCoreAPI): vendorID = data.pop('vendorID') vendor = self.api.set_vendor(vendorID, **data) return vendor + + +class ProductImporter(ToCoreAPI): + """ + Product model importer for CORE-POS + """ + model_name = 'Product' + key = 'upc' + supported_fields = [ + 'upc', + 'brand', + 'description', + 'size', + 'department', + 'normal_price', + 'foodstamp', + 'scale', + # 'tax', # TODO! + + # TODO: maybe enable some of these fields? + # 'formatted_name', + # 'pricemethod', + # 'groupprice', + # 'quantity', + # 'special_price', + # 'specialpricemethod', + # 'specialgroupprice', + # 'specialquantity', + # 'start_date', + # 'end_date', + # 'scaleprice', + # 'mixmatchcode', + # 'modified', + # 'tareweight', + # 'discount', + # 'discounttype', + # 'line_item_discountable', + # 'unitofmeasure', + # 'wicable', + # 'qttyEnforced', + # 'idEnforced', + # 'cost', + # 'inUse', + # 'numflag', + # 'subdept', + # 'deposit', + # 'local', + # 'store_id', + # 'default_vendor_id', + # 'current_origin_id', + ] + + def get_local_objects(self, host_data=None): + return self.api.get_products() + + def get_single_local_object(self, key): + assert len(self.key) == 1 + assert self.key[0] == 'upc' + return self.api.get_product(key[0]) + + def normalize_local_object(self, product): + data = dict(product) + + # make sure all fields are present + self.ensure_fields(data) + + # fix some "empty" values + self.fix_empties(data, ['brand']) + + return data + + def create_object(self, key, data): + # we can get away with using the same logic for both here + return self.update_object(None, data) + + def update_object(self, product, data, local_data=None): + """ + Push an update for the product, via the CORE API. + """ + if self.dry_run: + return data + + upc = data.pop('upc') + product = self.api.set_product(upc, **data) + return product diff --git a/rattail_corepos/corepos/importing/rattail.py b/rattail_corepos/corepos/importing/rattail.py index cc8e253..eb68191 100644 --- a/rattail_corepos/corepos/importing/rattail.py +++ b/rattail_corepos/corepos/importing/rattail.py @@ -47,7 +47,10 @@ class FromRattailToCore(importing.FromRattailHandler): def get_importers(self): importers = OrderedDict() + importers['Department'] = DepartmentImporter + importers['Subdepartment'] = SubdepartmentImporter importers['Vendor'] = VendorImporter + importers['Product'] = ProductImporter return importers @@ -57,6 +60,45 @@ class FromRattail(importing.FromSQLAlchemy): """ +class DepartmentImporter(FromRattail, corepos_importing.model.DepartmentImporter): + """ + Department data exporter + """ + host_model_class = model.Department + key = 'dept_no' + supported_fields = [ + 'dept_no', + 'dept_name', + ] + + def normalize_host_object(self, department): + return { + 'dept_no': str(department.number), + 'dept_name': department.name, + } + + +class SubdepartmentImporter(FromRattail, corepos_importing.model.SubdepartmentImporter): + """ + Subdepartment data exporter + """ + host_model_class = model.Subdepartment + key = 'subdept_no' + supported_fields = [ + 'subdept_no', + 'subdept_name', + 'dept_ID', + ] + + def normalize_host_object(self, subdepartment): + department = subdepartment.department + return { + 'subdept_no': str(subdepartment.number), + 'subdept_name': subdepartment.name, + 'dept_ID': str(department.number) if department else None, + } + + class VendorImporter(FromRattail, corepos_importing.model.VendorImporter): """ Vendor data exporter @@ -135,3 +177,37 @@ class VendorImporter(FromRattail, corepos_importing.model.VendorImporter): rattail_vendor.corepos_id = int(vendor['vendorID']) return vendor + + +class ProductImporter(FromRattail, corepos_importing.model.ProductImporter): + """ + Product data exporter + """ + host_model_class = model.Product + key = 'upc' + supported_fields = [ + 'upc', + 'brand', + 'description', + 'size', + 'department', + 'normal_price', + 'foodstamp', + 'scale', + ] + + def query(self): + query = super(ProductImporter, self).query() + return query.filter(model.Product.item_id != None) + + def normalize_host_object(self, product): + return { + 'upc': product.item_id, + 'brand': product.brand.name if product.brand else '', + 'description': product.description or None, + 'size': product.size, + 'department': str(product.department.number) if product.department else None, + 'normal_price': '{:0.2f}'.format(product.regular_price.price) if product.regular_price else None, + 'foodstamp': '1' if product.food_stampable else '0', + 'scale': '1' if product.weighed else '0', + } diff --git a/rattail_corepos/datasync/corepos.py b/rattail_corepos/datasync/corepos.py index 8c8423f..22ab69e 100644 --- a/rattail_corepos/datasync/corepos.py +++ b/rattail_corepos/datasync/corepos.py @@ -152,6 +152,22 @@ class FromRattailToCore(NewDataSyncImportConsumer): # also establish the API client for each! importer.establish_api() + # sync all Department changes + types = [ + 'Department', + ] + for change in [c for c in changes if c.payload_type in types]: + if change.payload_type == 'Department' and change.deletion: + # TODO: we have no way (yet) to delete a CORE department via API + # # just do default logic for this one + # self.invoke_importer(session, change) + pass + else: # we consider this an "add/update" + department = self.get_department(session, change) + if department: + self.process_change(session, self.importers['Department'], + host_object=department) + # sync all Vendor changes types = [ 'Vendor', @@ -164,13 +180,17 @@ class FromRattailToCore(NewDataSyncImportConsumer): # self.invoke_importer(session, change) # TODO: we have no way to delete a CORE vendor via API, right? pass - else: # we consider this a "vendor add/update" - vendor = self.get_host_vendor(session, change) + else: # we consider this an "add/update" + vendor = self.get_vendor(session, change) if vendor: self.process_change(session, self.importers['Vendor'], host_object=vendor) - def get_host_vendor(self, session, change): + def get_department(self, session, change): + if change.payload_type == 'Department': + return session.query(model.Department).get(change.payload_key) + + def get_vendor(self, session, change): if change.payload_type == 'Vendor': return session.query(model.Vendor).get(change.payload_key) diff --git a/rattail_corepos/importing/corepos/api.py b/rattail_corepos/importing/corepos/api.py index 2c44f0e..5a63d86 100644 --- a/rattail_corepos/importing/corepos/api.py +++ b/rattail_corepos/importing/corepos/api.py @@ -102,10 +102,14 @@ class SubdepartmentImporter(FromCOREPOSAPI, importing.model.SubdepartmentImporte return self.api.get_subdepartments() def normalize_host_object(self, subdepartment): + department_number = None + if 'dept_ID' in subdepartment: + department_number = int(subdepartment['dept_ID']) + return { 'number': int(subdepartment['subdept_no']), 'name': subdepartment['subdept_name'], - 'department_number': int(subdepartment['dept_ID']), + 'department_number': department_number, } @@ -157,8 +161,8 @@ class ProductImporter(FromCOREPOSAPI, importing.model.ProductImporter): 'regular_price_multiple', 'regular_price_type', 'food_stampable', - 'tax1', - 'tax2', + # 'tax1', + # 'tax2', ] def get_host_objects(self): @@ -173,28 +177,32 @@ class ProductImporter(FromCOREPOSAPI, importing.model.ProductImporter): return upc = None + department_number = None + if 'department' in product: + department_number = int(product['department']) + + subdepartment_number = None + if 'subdept' in product: + subdepartment_number = int(product['subdept']) or None + price = None if product['normal_price'] is not None: price = decimal.Decimal(product['normal_price']) - size = product.get('size', '').strip() or None - if size == '0': # TODO: this is maybe just for sake of CORE sample data? - size = None - return { 'item_id': product['upc'], 'upc': upc, 'brand_name': product.get('brand') or None, 'description': product.get('description') or '', - 'size': size, + 'size': product.get('size', '').strip() or None, - 'department_number': int(product['department']) or None, - 'subdepartment_number': int(product['subdept']) or None, + 'department_number': department_number, + 'subdepartment_number': subdepartment_number, 'weighed': product['scale'] == '1', 'food_stampable': product['foodstamp'] == '1', - 'tax1': product['tax'] == '1', # TODO: is this right? - 'tax2': product['tax'] == '2', # TODO: is this right? + # 'tax1': product['tax'] == '1', # TODO: is this right? + # 'tax2': product['tax'] == '2', # TODO: is this right? 'regular_price_price': price, 'regular_price_multiple': 1 if price is not None else None,