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):
importers = OrderedDict()
importers['Customer'] = CustomerImporter
importers['Person'] = PersonImporter
importers['CustomerPerson'] = CustomerPersonImporter
importers['CustomerShopper'] = CustomerShopperImporter
importers['MembershipType'] = MembershipTypeImporter
importers['Member'] = MemberImporter
importers['Store'] = StoreImporter
@ -72,11 +71,6 @@ class FromCOREPOSToRattail(importing.ToRattailHandler):
def get_default_keys(self):
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:
keys.remove('ProductMovement')
return keys
@ -88,18 +82,51 @@ class FromCOREPOSAPI(importing.Importer):
"""
def setup(self):
super(FromCOREPOSAPI, self).setup()
super().setup()
self.establish_api()
self.ignore_new_members = self.should_ignore_new_members()
def datasync_setup(self):
super(FromCOREPOSAPI, self).datasync_setup()
super().datasync_setup()
self.establish_api()
def establish_api(self):
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):
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):
@ -111,11 +138,11 @@ class CustomerImporter(FromCOREPOSAPI, corepos_importing.model.CustomerImporter)
'corepos_card_number',
'number',
'name',
'address_street',
'address_street2',
'address_city',
'address_state',
'address_zipcode',
# 'address_street',
# 'address_street2',
# 'address_city',
# 'address_state',
# 'address_zipcode',
]
def get_host_objects(self):
@ -148,71 +175,77 @@ class CustomerImporter(FromCOREPOSAPI, corepos_importing.model.CustomerImporter)
'name': normalize_full_name(customer['firstName'],
customer['lastName']),
'address_street': member['addressFirstLine'] or None,
'address_street2': member['addressSecondLine'] or None,
'address_city': member['city'] or None,
'address_state': member['state'] or None,
'address_zipcode': member['zip'] or None,
# 'address_street': member['addressFirstLine'] or None,
# 'address_street2': member['addressSecondLine'] or None,
# 'address_city': member['city'] or None,
# 'address_state': member['state'] 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 = [
'customer_uuid',
'customer_person_ordinal',
'shopper_number',
'first_name',
'last_name',
'display_name',
'phone_number',
'phone_number_2',
'email_address',
'active',
# 'phone_number',
# 'phone_number_2',
# 'email_address',
]
def setup(self):
super().setup()
model = self.model
# self.maxlen_phone_number = self.app.maxlen(model.PhoneNumber.number)
self.customers_by_card_number = self.app.cache_model(
self.session,
model.Customer,
key='corepos_card_number',
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):
# first get all member data from CORE API
members = self.get_core_members()
normalized = []
# then collect all the "person" records
# then collect all the "shopper" records
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,
message="Collecting Person data from CORE")
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
logic is split out separately so that datasync can leverage it too.
Return a list of shopper info dicts associated with the given
member info dict (latter having come from the CORE API).
"""
customers = member['customers']
people = []
shoppers = []
# make sure account holder is listed first
account_holder = None
@ -228,198 +261,62 @@ class PersonImporter(FromCOREPOSAPI, corepos_importing.model.PersonImporter):
if mixedup:
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):
person = dict(customer)
shopper = dict(customer)
shopper['card_number'] = member['cardNo']
shopper['shopper_number'] = i
shoppers.append(shopper)
if (ignore_new_members
and not customer['firstName']
and customer['lastName'] == 'NEW MEMBER'):
return shoppers
log.debug("ignoring new member #%s: %s",
member['cardNo'], member)
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']
def normalize_host_object(self, shopper):
card_number = shopper['card_number']
customer = self.get_customer_by_card_number(card_number)
if not customer:
log.warning("Rattail customer not found for CardNo %s: %s",
card_number, person)
card_number, shopper)
return
data = {
'customer_uuid': customer.uuid,
'customer_person_ordinal': person['customer_person_ordinal'],
'shopper_number': shopper['shopper_number'],
'first_name': person['firstName'],
'last_name': person['lastName'],
'display_name': normalize_full_name(person['firstName'],
person['lastName']),
'first_name': shopper['firstName'],
'last_name': shopper['lastName'],
'display_name': normalize_full_name(shopper['firstName'],
shopper['lastName']),
'phone_number': person['phone'] or None,
'phone_number_2': person['altPhone'] or None,
'email_address': person['email'] or None,
# TODO: can a CORE shopper be *not* active?
'active': True,
# '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
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",
len(data['phone_number']),
self.maxlen_phone_number,
data['phone_number'])
data['phone_number'] = data['phone_number'][:self.maxlen_phone_number]
if data['phone_number_2'] and len(data['phone_number_2']) > self.maxlen_phone_number:
log.warning("phone_number_2 is too long (%s chars), will truncate to %s chars: %s",
len(data['phone_number_2']),
self.maxlen_phone_number,
data['phone_number_2'])
data['phone_number_2'] = data['phone_number_2'][:self.maxlen_phone_number]
# # truncate phone number data if needed
# 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",
# len(data['phone_number']),
# self.maxlen_phone_number,
# data['phone_number'])
# data['phone_number'] = data['phone_number'][:self.maxlen_phone_number]
# if data['phone_number_2'] and len(data['phone_number_2']) > self.maxlen_phone_number:
# log.warning("phone_number_2 is too long (%s chars), "
# "will truncate to %s chars: %s",
# len(data['phone_number_2']),
# 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
self.prioritize_2(data, 'phone_number')
# # swap 1st and 2nd phone numbers if only latter has value
# self.prioritize_2(data, 'phone_number')
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):
"""
@ -920,7 +817,7 @@ class MemberImporter(FromCOREPOSAPI, corepos_importing.model.MemberImporter):
card_number, member)
return
person = customer.first_person()
person = self.app.get_person(customer)
if not person:
log.warning("Rattail person not found for cardNo %s: %s",
card_number, member)