Add initial/basic CustomerShopper importer for CORE -> Rattail

this replaces previous importers for Person and CustomerPerson

no contact info support just yet..need to decide where to put that
This commit is contained in:
Lance Edgar 2023-06-07 16:37:26 -05:00
parent 2c38e4d5d3
commit a45d28cd6f

View file

@ -56,8 +56,7 @@ class FromCOREPOSToRattail(importing.ToRattailHandler):
def get_importers(self): def get_importers(self):
importers = OrderedDict() importers = OrderedDict()
importers['Customer'] = CustomerImporter importers['Customer'] = CustomerImporter
importers['Person'] = PersonImporter importers['CustomerShopper'] = CustomerShopperImporter
importers['CustomerPerson'] = CustomerPersonImporter
importers['MembershipType'] = MembershipTypeImporter importers['MembershipType'] = MembershipTypeImporter
importers['Member'] = MemberImporter importers['Member'] = MemberImporter
importers['Store'] = StoreImporter importers['Store'] = StoreImporter
@ -72,11 +71,6 @@ class FromCOREPOSToRattail(importing.ToRattailHandler):
def get_default_keys(self): def get_default_keys(self):
keys = super(FromCOREPOSToRattail, self).get_default_keys() keys = super(FromCOREPOSToRattail, self).get_default_keys()
# normally this one is redundant, but it can be used for
# double-check if desired
if 'CustomerPerson' in keys:
keys.remove('CustomerPerson')
if 'ProductMovement' in keys: if 'ProductMovement' in keys:
keys.remove('ProductMovement') keys.remove('ProductMovement')
return keys return keys
@ -88,18 +82,51 @@ class FromCOREPOSAPI(importing.Importer):
""" """
def setup(self): def setup(self):
super(FromCOREPOSAPI, self).setup() super().setup()
self.establish_api() self.establish_api()
self.ignore_new_members = self.should_ignore_new_members()
def datasync_setup(self): def datasync_setup(self):
super(FromCOREPOSAPI, self).datasync_setup() super().datasync_setup()
self.establish_api() self.establish_api()
def establish_api(self): def establish_api(self):
self.api = make_corepos_api(self.config) self.api = make_corepos_api(self.config)
def should_ignore_new_members(self):
if hasattr(self, 'ignore_new_members'):
return self.ignore_new_members
return self.config.getbool('rattail_corepos',
'importing_ignore_new_members',
default=False)
def get_core_members(self): def get_core_members(self):
return get_core_members(self.config, self.api, progress=self.progress) members = get_core_members(self.config, self.api, progress=self.progress)
# maybe ignore NEW MEMBER accounts
if self.should_ignore_new_members():
members = [member for member in members
if not self.is_new_member(member)]
return members
def is_new_member(self, member):
"""
Convenience method to check if the given member represents a
"NEW MEMBER" record in CORE-POS, and hence it should be
ignored for the import.
"""
customers = member['customers']
if customers:
customer = customers[0]
if (not customer['firstName']
and customer['lastName'] == 'NEW MEMBER'):
return True
return False
class CustomerImporter(FromCOREPOSAPI, corepos_importing.model.CustomerImporter): class CustomerImporter(FromCOREPOSAPI, corepos_importing.model.CustomerImporter):
@ -111,11 +138,11 @@ class CustomerImporter(FromCOREPOSAPI, corepos_importing.model.CustomerImporter)
'corepos_card_number', 'corepos_card_number',
'number', 'number',
'name', 'name',
'address_street', # 'address_street',
'address_street2', # 'address_street2',
'address_city', # 'address_city',
'address_state', # 'address_state',
'address_zipcode', # 'address_zipcode',
] ]
def get_host_objects(self): def get_host_objects(self):
@ -148,71 +175,77 @@ class CustomerImporter(FromCOREPOSAPI, corepos_importing.model.CustomerImporter)
'name': normalize_full_name(customer['firstName'], 'name': normalize_full_name(customer['firstName'],
customer['lastName']), customer['lastName']),
'address_street': member['addressFirstLine'] or None, # 'address_street': member['addressFirstLine'] or None,
'address_street2': member['addressSecondLine'] or None, # 'address_street2': member['addressSecondLine'] or None,
'address_city': member['city'] or None, # 'address_city': member['city'] or None,
'address_state': member['state'] or None, # 'address_state': member['state'] or None,
'address_zipcode': member['zip'] or None, # 'address_zipcode': member['zip'] or None,
} }
class PersonImporter(FromCOREPOSAPI, corepos_importing.model.PersonImporter): class CustomerShopperImporter(FromCOREPOSAPI, importing.model.CustomerShopperImporter):
""" """
Importer for person data from CORE POS API. Importer for customer shopper data from CORE POS API.
""" """
key = ('customer_uuid', 'customer_person_ordinal') key = ('customer_uuid', 'shopper_number')
supported_fields = [ supported_fields = [
'customer_uuid', 'customer_uuid',
'customer_person_ordinal', 'shopper_number',
'first_name', 'first_name',
'last_name', 'last_name',
'display_name', 'display_name',
'phone_number', 'active',
'phone_number_2', # 'phone_number',
'email_address', # 'phone_number_2',
# 'email_address',
] ]
def setup(self): def setup(self):
super().setup() super().setup()
model = self.model model = self.model
# self.maxlen_phone_number = self.app.maxlen(model.PhoneNumber.number)
self.customers_by_card_number = self.app.cache_model( self.customers_by_card_number = self.app.cache_model(
self.session, self.session,
model.Customer, model.Customer,
key='corepos_card_number', key='corepos_card_number',
query_options=[orm.joinedload(model.Customer._corepos)]) query_options=[orm.joinedload(model.Customer._corepos)])
self.ignore_new_members = self.should_ignore_new_members()
def should_ignore_new_members(self):
if hasattr(self, 'ignore_new_members'):
return self.ignore_new_members
return self.config.getbool('rattail_corepos',
'importing_ignore_new_members',
default=False)
def get_host_objects(self): def get_host_objects(self):
# first get all member data from CORE API # first get all member data from CORE API
members = self.get_core_members() members = self.get_core_members()
normalized = [] normalized = []
# then collect all the "person" records # then collect all the "shopper" records
def normalize(member, i): def normalize(member, i):
normalized.extend(self.get_person_objects_for_member(member)) normalized.extend(self.get_shoppers_for_member(member))
self.progress_loop(normalize, members, self.progress_loop(normalize, members,
message="Collecting Person data from CORE") message="Collecting Person data from CORE")
return normalized return normalized
def get_person_objects_for_member(self, member): def get_customer_by_card_number(self, card_number):
if hasattr(self, 'customers_by_card_number'):
return self.customers_by_card_number.get(card_number)
model = self.model
try:
return self.session.query(model.Customer)\
.join(model.CoreCustomer)\
.filter(model.CoreCustomer.corepos_card_number == card_number)\
.one()
except orm.exc.NoResultFound:
pass
def get_shoppers_for_member(self, member):
""" """
Return a list of Person data objects for the given Member. This Return a list of shopper info dicts associated with the given
logic is split out separately so that datasync can leverage it too. member info dict (latter having come from the CORE API).
""" """
customers = member['customers'] customers = member['customers']
people = [] shoppers = []
# make sure account holder is listed first # make sure account holder is listed first
account_holder = None account_holder = None
@ -228,198 +261,62 @@ class PersonImporter(FromCOREPOSAPI, corepos_importing.model.PersonImporter):
if mixedup: if mixedup:
raise NotImplementedError("TODO: should re-sort the customers list for member {}".format(member['cardNo'])) raise NotImplementedError("TODO: should re-sort the customers list for member {}".format(member['cardNo']))
ignore_new_members = self.should_ignore_new_members()
for i, customer in enumerate(customers, 1): for i, customer in enumerate(customers, 1):
person = dict(customer) shopper = dict(customer)
shopper['card_number'] = member['cardNo']
shopper['shopper_number'] = i
shoppers.append(shopper)
if (ignore_new_members return shoppers
and not customer['firstName']
and customer['lastName'] == 'NEW MEMBER'):
log.debug("ignoring new member #%s: %s", def normalize_host_object(self, shopper):
member['cardNo'], member) card_number = shopper['card_number']
else:
person['member_card_number'] = member['cardNo']
person['customer_person_ordinal'] = i
people.append(person)
return people
def get_customer_by_card_number(self, card_number):
if hasattr(self, 'customers_by_card_number'):
return self.customers_by_card_number.get(card_number)
model = self.model
try:
return self.session.query(model.Customer)\
.join(model.CoreCustomer)\
.filter(model.CoreCustomer.corepos_card_number == card_number)\
.one()
except orm.exc.NoResultFound:
pass
def normalize_host_object(self, person):
card_number = person['member_card_number']
customer = self.get_customer_by_card_number(card_number) customer = self.get_customer_by_card_number(card_number)
if not customer: if not customer:
log.warning("Rattail customer not found for CardNo %s: %s", log.warning("Rattail customer not found for CardNo %s: %s",
card_number, person) card_number, shopper)
return return
data = { data = {
'customer_uuid': customer.uuid, 'customer_uuid': customer.uuid,
'customer_person_ordinal': person['customer_person_ordinal'], 'shopper_number': shopper['shopper_number'],
'first_name': person['firstName'], 'first_name': shopper['firstName'],
'last_name': person['lastName'], 'last_name': shopper['lastName'],
'display_name': normalize_full_name(person['firstName'], 'display_name': normalize_full_name(shopper['firstName'],
person['lastName']), shopper['lastName']),
'phone_number': person['phone'] or None, # TODO: can a CORE shopper be *not* active?
'phone_number_2': person['altPhone'] or None, 'active': True,
'email_address': person['email'] or None,
# 'phone_number': shopper['phone'] or None,
# 'phone_number_2': shopper['altPhone'] or None,
# 'email_address': shopper['email'] or None,
} }
# truncate phone number data if needed # # truncate phone number data if needed
if data['phone_number'] and len(data['phone_number']) > self.maxlen_phone_number: # if data['phone_number'] and len(data['phone_number']) > self.maxlen_phone_number:
log.warning("phone_number is too long (%s chars), will truncate to %s chars: %s", # log.warning("phone_number is too long (%s chars), "
len(data['phone_number']), # "will truncate to %s chars: %s",
self.maxlen_phone_number, # len(data['phone_number']),
data['phone_number']) # self.maxlen_phone_number,
data['phone_number'] = data['phone_number'][:self.maxlen_phone_number] # data['phone_number'])
if data['phone_number_2'] and len(data['phone_number_2']) > self.maxlen_phone_number: # data['phone_number'] = data['phone_number'][:self.maxlen_phone_number]
log.warning("phone_number_2 is too long (%s chars), will truncate to %s chars: %s", # if data['phone_number_2'] and len(data['phone_number_2']) > self.maxlen_phone_number:
len(data['phone_number_2']), # log.warning("phone_number_2 is too long (%s chars), "
self.maxlen_phone_number, # "will truncate to %s chars: %s",
data['phone_number_2']) # len(data['phone_number_2']),
data['phone_number_2'] = data['phone_number_2'][:self.maxlen_phone_number] # self.maxlen_phone_number,
# data['phone_number_2'])
# data['phone_number_2'] = data['phone_number_2'][:self.maxlen_phone_number]
# swap 1st and 2nd phone numbers if only latter has value # # swap 1st and 2nd phone numbers if only latter has value
self.prioritize_2(data, 'phone_number') # self.prioritize_2(data, 'phone_number')
return data return data
def normalize_local_object(self, person):
data = super().normalize_local_object(person)
if data:
# ignore local Person records with no customer_uuid;
# otherwise importer will try to delete them, and/or
# they just cause "duplicate keys" noise
if 'customer_uuid' in self.key:
if not data['customer_uuid']:
return
return data
class CustomerPersonImporter(FromCOREPOSAPI, importing.model.CustomerPersonImporter):
"""
Importer for customer-person linkage data from CORE POS API.
Note that we don't use this one in datasync, it's just for nightly
double-check.
"""
key = ('customer_uuid', 'person_uuid')
supported_fields = [
'customer_uuid',
'person_uuid',
'ordinal',
]
def setup(self):
super(CustomerPersonImporter, self).setup()
model = self.config.get_model()
query = self.session.query(model.Customer)\
.join(model.CoreCustomer)
self.customers = self.app.cache_model(self.session,
model.Customer,
query=query,
key='corepos_account_id')
query = self.session.query(model.Person)\
.join(model.CorePerson)\
.filter(model.CorePerson.corepos_customer_id != None)
self.people = self.app.cache_model(
self.session,
model.Person,
key='corepos_customer_id',
query=query,
query_options=[orm.joinedload(model.Person._corepos)])
def get_host_objects(self):
# first get all member data from CORE API
members = self.get_core_members()
normalized = []
# then collect all customer/person combination records
def normalize(member, i):
# make sure we put the account holder first in the list!
customers = sorted(member['customers'],
key=lambda cust: 1 if cust['accountHolder'] else 0,
reverse=True)
for i, customer in enumerate(customers, 1):
normalized.append({
'customer_account_id': int(member['customerAccountID']),
'person_customer_id': customer['customerID'],
'ordinal': i,
})
self.progress_loop(normalize, members,
message="Collecting CustomerPerson data from CORE")
return normalized
def get_customer(self, account_id):
if hasattr(self, 'customers'):
return self.customers.get(account_id)
model = self.config.get_model()
try:
return self.session.query(model.Customer)\
.join(model.CoreCustomer)\
.filter(model.CoreCustomer.corepos_account_id == account_id)\
.one()
except orm.exc.NoResultFound:
pass
def get_person(self, corepos_customer_id):
if hasattr(self, 'people'):
return self.people.get(corepos_customer_id)
model = self.config.get_model()
try:
return self.session.query(model.Person)\
.join(model.CorePerson)\
.filter(model.CorePerson.corepos_customer_id == corepos_customer_id)\
.one()
except orm.exc.NoResultFound:
pass
def normalize_host_object(self, cp):
customer = self.get_customer(cp['customer_account_id'])
if not customer:
log.warning("Rattail customer not found for customerAccountID: %s",
cp['customer_account_id'])
return
person = self.get_person(int(cp['person_customer_id']))
if not person:
log.warning("Rattail person not found for customerID: %s",
cp['person_customer_id'])
return
return {
'customer_uuid': customer.uuid,
'person_uuid': person.uuid,
'ordinal': cp['ordinal'],
}
class StoreImporter(FromCOREPOSAPI, corepos_importing.model.StoreImporter): class StoreImporter(FromCOREPOSAPI, corepos_importing.model.StoreImporter):
""" """
@ -920,7 +817,7 @@ class MemberImporter(FromCOREPOSAPI, corepos_importing.model.MemberImporter):
card_number, member) card_number, member)
return return
person = customer.first_person() person = self.app.get_person(customer)
if not person: if not person:
log.warning("Rattail person not found for cardNo %s: %s", log.warning("Rattail person not found for cardNo %s: %s",
card_number, member) card_number, member)