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:
parent
2c38e4d5d3
commit
a45d28cd6f
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue