diff --git a/rattail_corepos/importing/corepos/api.py b/rattail_corepos/importing/corepos/api.py index 1c0612e..4f58ba9 100644 --- a/rattail_corepos/importing/corepos/api.py +++ b/rattail_corepos/importing/corepos/api.py @@ -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)