Add support for Department, Subdepartment, Product in Rattail -> CORE API

This commit is contained in:
Lance Edgar 2020-03-15 15:54:15 -05:00
parent ab8894ef0d
commit cd93d3e36b
4 changed files with 301 additions and 18 deletions

View file

@ -33,6 +33,11 @@ class ToCoreAPI(importing.Importer):
""" """
Base class for all CORE "operational" model importers, which use the API. 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 caches_local_data = True
def setup(self): def setup(self):
@ -69,6 +74,98 @@ class ToCoreAPI(importing.Importer):
data[field] = '' 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): class VendorImporter(ToCoreAPI):
""" """
Vendor model importer for CORE-POS Vendor model importer for CORE-POS
@ -95,9 +192,6 @@ class VendorImporter(ToCoreAPI):
'orderMinimum', 'orderMinimum',
'halfCases', '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): def get_local_objects(self, host_data=None):
return self.api.get_vendors() return self.api.get_vendors()
@ -135,3 +229,88 @@ class VendorImporter(ToCoreAPI):
vendorID = data.pop('vendorID') vendorID = data.pop('vendorID')
vendor = self.api.set_vendor(vendorID, **data) vendor = self.api.set_vendor(vendorID, **data)
return vendor 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

View file

@ -47,7 +47,10 @@ class FromRattailToCore(importing.FromRattailHandler):
def get_importers(self): def get_importers(self):
importers = OrderedDict() importers = OrderedDict()
importers['Department'] = DepartmentImporter
importers['Subdepartment'] = SubdepartmentImporter
importers['Vendor'] = VendorImporter importers['Vendor'] = VendorImporter
importers['Product'] = ProductImporter
return importers 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): class VendorImporter(FromRattail, corepos_importing.model.VendorImporter):
""" """
Vendor data exporter Vendor data exporter
@ -135,3 +177,37 @@ class VendorImporter(FromRattail, corepos_importing.model.VendorImporter):
rattail_vendor.corepos_id = int(vendor['vendorID']) rattail_vendor.corepos_id = int(vendor['vendorID'])
return vendor 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',
}

View file

@ -152,6 +152,22 @@ class FromRattailToCore(NewDataSyncImportConsumer):
# also establish the API client for each! # also establish the API client for each!
importer.establish_api() 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 # sync all Vendor changes
types = [ types = [
'Vendor', 'Vendor',
@ -164,13 +180,17 @@ class FromRattailToCore(NewDataSyncImportConsumer):
# self.invoke_importer(session, change) # self.invoke_importer(session, change)
# TODO: we have no way to delete a CORE vendor via API, right? # TODO: we have no way to delete a CORE vendor via API, right?
pass pass
else: # we consider this a "vendor add/update" else: # we consider this an "add/update"
vendor = self.get_host_vendor(session, change) vendor = self.get_vendor(session, change)
if vendor: if vendor:
self.process_change(session, self.importers['Vendor'], self.process_change(session, self.importers['Vendor'],
host_object=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': if change.payload_type == 'Vendor':
return session.query(model.Vendor).get(change.payload_key) return session.query(model.Vendor).get(change.payload_key)

View file

@ -102,10 +102,14 @@ class SubdepartmentImporter(FromCOREPOSAPI, importing.model.SubdepartmentImporte
return self.api.get_subdepartments() return self.api.get_subdepartments()
def normalize_host_object(self, subdepartment): def normalize_host_object(self, subdepartment):
department_number = None
if 'dept_ID' in subdepartment:
department_number = int(subdepartment['dept_ID'])
return { return {
'number': int(subdepartment['subdept_no']), 'number': int(subdepartment['subdept_no']),
'name': subdepartment['subdept_name'], '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_multiple',
'regular_price_type', 'regular_price_type',
'food_stampable', 'food_stampable',
'tax1', # 'tax1',
'tax2', # 'tax2',
] ]
def get_host_objects(self): def get_host_objects(self):
@ -173,28 +177,32 @@ class ProductImporter(FromCOREPOSAPI, importing.model.ProductImporter):
return return
upc = None 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 price = None
if product['normal_price'] is not None: if product['normal_price'] is not None:
price = decimal.Decimal(product['normal_price']) 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 { return {
'item_id': product['upc'], 'item_id': product['upc'],
'upc': upc, 'upc': upc,
'brand_name': product.get('brand') or None, 'brand_name': product.get('brand') or None,
'description': product.get('description') or '', 'description': product.get('description') or '',
'size': size, 'size': product.get('size', '').strip() or None,
'department_number': int(product['department']) or None, 'department_number': department_number,
'subdepartment_number': int(product['subdept']) or None, 'subdepartment_number': subdepartment_number,
'weighed': product['scale'] == '1', 'weighed': product['scale'] == '1',
'food_stampable': product['foodstamp'] == '1', 'food_stampable': product['foodstamp'] == '1',
'tax1': product['tax'] == '1', # TODO: is this right? # 'tax1': product['tax'] == '1', # TODO: is this right?
'tax2': product['tax'] == '2', # TODO: is this right? # 'tax2': product['tax'] == '2', # TODO: is this right?
'regular_price_price': price, 'regular_price_price': price,
'regular_price_multiple': 1 if price is not None else None, 'regular_price_multiple': 1 if price is not None else None,