Add 2-way sync for basic Member data, CORE <-> Rattail
This commit is contained in:
		
							parent
							
								
									9dbdb81f07
								
							
						
					
					
						commit
						8917316a21
					
				
					 5 changed files with 158 additions and 14 deletions
				
			
		| 
						 | 
					@ -92,8 +92,8 @@ class MemberImporter(ToCoreAPI):
 | 
				
			||||||
        # 'chargeBalance',
 | 
					        # 'chargeBalance',
 | 
				
			||||||
        # 'chargeLimit',
 | 
					        # 'chargeLimit',
 | 
				
			||||||
        # 'idCardUPC',
 | 
					        # 'idCardUPC',
 | 
				
			||||||
        # 'startDate',
 | 
					        'startDate',
 | 
				
			||||||
        # 'endDate',
 | 
					        'endDate',
 | 
				
			||||||
        'addressFirstLine',
 | 
					        'addressFirstLine',
 | 
				
			||||||
        'addressSecondLine',
 | 
					        'addressSecondLine',
 | 
				
			||||||
        'city',
 | 
					        'city',
 | 
				
			||||||
| 
						 | 
					@ -123,6 +123,8 @@ class MemberImporter(ToCoreAPI):
 | 
				
			||||||
        # 'modified',
 | 
					        # 'modified',
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    empty_date_value = '0000-00-00 00:00:00'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_local_objects(self, host_data=None):
 | 
					    def get_local_objects(self, host_data=None):
 | 
				
			||||||
        return get_core_members(self.api, progress=self.progress)
 | 
					        return get_core_members(self.api, progress=self.progress)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -144,6 +146,15 @@ class MemberImporter(ToCoreAPI):
 | 
				
			||||||
            if not self.customer_data_differs(local_data, host_data):
 | 
					            if not self.customer_data_differs(local_data, host_data):
 | 
				
			||||||
                diffs.remove('customers')
 | 
					                diffs.remove('customers')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # also the start/end dates should be looked at more closely.  if they
 | 
				
			||||||
 | 
					        # contain the special '__omit__' value then we won't ever count as diff
 | 
				
			||||||
 | 
					        if 'startDate' in self.fields and 'startDate' in diffs:
 | 
				
			||||||
 | 
					            if host_data['startDate'] == '__omit__':
 | 
				
			||||||
 | 
					                diffs.remove('startDate')
 | 
				
			||||||
 | 
					        if 'endDate' in self.fields and 'endDate' in diffs:
 | 
				
			||||||
 | 
					            if host_data['endDate'] == '__omit__':
 | 
				
			||||||
 | 
					                diffs.remove('endDate')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return diffs
 | 
					        return diffs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def customer_data_differs(self, local_data, host_data):
 | 
					    def customer_data_differs(self, local_data, host_data):
 | 
				
			||||||
| 
						 | 
					@ -211,6 +222,11 @@ class MemberImporter(ToCoreAPI):
 | 
				
			||||||
            return data
 | 
					            return data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        cardNo = data.pop('cardNo')
 | 
					        cardNo = data.pop('cardNo')
 | 
				
			||||||
 | 
					        data = dict(data)
 | 
				
			||||||
 | 
					        if data.get('startDate') == '__omit__':
 | 
				
			||||||
 | 
					            data.pop('startDate')
 | 
				
			||||||
 | 
					        if data.get('endDate') == '__omit__':
 | 
				
			||||||
 | 
					            data.pop('endDate')
 | 
				
			||||||
        member = self.api.set_member(cardNo, **data)
 | 
					        member = self.api.set_member(cardNo, **data)
 | 
				
			||||||
        return member
 | 
					        return member
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -78,6 +78,8 @@ class MemberImporter(FromRattail, corepos_importing.model.MemberImporter):
 | 
				
			||||||
        'city',
 | 
					        'city',
 | 
				
			||||||
        'state',
 | 
					        'state',
 | 
				
			||||||
        'zip',
 | 
					        'zip',
 | 
				
			||||||
 | 
					        'startDate',
 | 
				
			||||||
 | 
					        'endDate',
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
    supported_customer_fields = [
 | 
					    supported_customer_fields = [
 | 
				
			||||||
        'customerID',
 | 
					        'customerID',
 | 
				
			||||||
| 
						 | 
					@ -120,6 +122,20 @@ class MemberImporter(FromRattail, corepos_importing.model.MemberImporter):
 | 
				
			||||||
                'email': email.address if email else '',
 | 
					                'email': email.address if email else '',
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        member = customer.only_member(require=False)
 | 
				
			||||||
 | 
					        if member:
 | 
				
			||||||
 | 
					            if member.joined:
 | 
				
			||||||
 | 
					                start_date = member.joined.strftime('%Y-%m-%d 00:00:00')
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                start_date = self.empty_date_value
 | 
				
			||||||
 | 
					            if member.withdrew:
 | 
				
			||||||
 | 
					                end_date = member.withdrew.strftime('%Y-%m-%d 00:00:00')
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                end_date = self.empty_date_value
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            start_date = '__omit__'
 | 
				
			||||||
 | 
					            end_date = '__omit__'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
            'cardNo': customer.number,
 | 
					            'cardNo': customer.number,
 | 
				
			||||||
            'customerAccountID': customer.id,
 | 
					            'customerAccountID': customer.id,
 | 
				
			||||||
| 
						 | 
					@ -128,6 +144,8 @@ class MemberImporter(FromRattail, corepos_importing.model.MemberImporter):
 | 
				
			||||||
            'city': address.city if address else '',
 | 
					            'city': address.city if address else '',
 | 
				
			||||||
            'state': address.state if address else '',
 | 
					            'state': address.state if address else '',
 | 
				
			||||||
            'zip': address.zipcode if address else '',
 | 
					            'zip': address.zipcode if address else '',
 | 
				
			||||||
 | 
					            'startDate': start_date,
 | 
				
			||||||
 | 
					            'endDate': end_date,
 | 
				
			||||||
            'customers': people,
 | 
					            'customers': people,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -160,6 +160,7 @@ class FromRattailToCore(NewDataSyncImportConsumer):
 | 
				
			||||||
            'CustomerMailingAddress',
 | 
					            'CustomerMailingAddress',
 | 
				
			||||||
            'PersonPhoneNumber',
 | 
					            'PersonPhoneNumber',
 | 
				
			||||||
            'PersonEmailAddress',
 | 
					            'PersonEmailAddress',
 | 
				
			||||||
 | 
					            'Member',
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
        for change in [c for c in changes if c.payload_type in types]:
 | 
					        for change in [c for c in changes if c.payload_type in types]:
 | 
				
			||||||
            if change.payload_type == 'Customer' and change.deletion:
 | 
					            if change.payload_type == 'Customer' and change.deletion:
 | 
				
			||||||
| 
						 | 
					@ -252,6 +253,11 @@ class FromRattailToCore(NewDataSyncImportConsumer):
 | 
				
			||||||
            if email:
 | 
					            if email:
 | 
				
			||||||
                return email.person.customers
 | 
					                return email.person.customers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if change.payload_type == 'Member':
 | 
				
			||||||
 | 
					            member = session.query(model.Member).get(change.payload_key)
 | 
				
			||||||
 | 
					            if member:
 | 
				
			||||||
 | 
					                return [member.customer]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return []
 | 
					        return []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_vendor(self, session, change):
 | 
					    def get_vendor(self, session, change):
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -73,6 +73,8 @@ class FromCOREAPIToRattail(NewDataSyncImportConsumer):
 | 
				
			||||||
                for person in people:
 | 
					                for person in people:
 | 
				
			||||||
                    self.process_change(session, self.importers['Person'],
 | 
					                    self.process_change(session, self.importers['Person'],
 | 
				
			||||||
                                        host_object=person)
 | 
					                                        host_object=person)
 | 
				
			||||||
 | 
					                self.process_change(session, self.importers['Member'],
 | 
				
			||||||
 | 
					                                    host_object=member)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # process all remaining supported models with typical logic
 | 
					        # process all remaining supported models with typical logic
 | 
				
			||||||
        types = [
 | 
					        types = [
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -24,6 +24,7 @@
 | 
				
			||||||
CORE POS (API) -> Rattail data importing
 | 
					CORE POS (API) -> Rattail data importing
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import datetime
 | 
				
			||||||
import decimal
 | 
					import decimal
 | 
				
			||||||
import logging
 | 
					import logging
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -55,6 +56,7 @@ class FromCOREPOSToRattail(importing.ToRattailHandler):
 | 
				
			||||||
        importers['Customer'] = CustomerImporter
 | 
					        importers['Customer'] = CustomerImporter
 | 
				
			||||||
        importers['Person'] = PersonImporter
 | 
					        importers['Person'] = PersonImporter
 | 
				
			||||||
        importers['CustomerPerson'] = CustomerPersonImporter
 | 
					        importers['CustomerPerson'] = CustomerPersonImporter
 | 
				
			||||||
 | 
					        importers['Member'] = MemberImporter
 | 
				
			||||||
        importers['Department'] = DepartmentImporter
 | 
					        importers['Department'] = DepartmentImporter
 | 
				
			||||||
        importers['Subdepartment'] = SubdepartmentImporter
 | 
					        importers['Subdepartment'] = SubdepartmentImporter
 | 
				
			||||||
        importers['Vendor'] = VendorImporter
 | 
					        importers['Vendor'] = VendorImporter
 | 
				
			||||||
| 
						 | 
					@ -100,20 +102,32 @@ class CustomerImporter(FromCOREPOSAPI, importing.model.CustomerImporter):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def normalize_host_object(self, member):
 | 
					    def normalize_host_object(self, member):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # figure out the "account holder" customer for the member
 | 
					        if member['customerAccountID'] == 0:
 | 
				
			||||||
 | 
					            log.debug("member %s has customerAccountID of 0: %s",
 | 
				
			||||||
 | 
					                      member['cardNo'], member)
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # figure out the "account holder" customer for the member.  note that
 | 
				
			||||||
 | 
					        # we only use this to determine the `Customer.name` in Rattail
 | 
				
			||||||
        customers = member['customers']
 | 
					        customers = member['customers']
 | 
				
			||||||
        account_holders = [customer for customer in customers
 | 
					        account_holders = [customer for customer in customers
 | 
				
			||||||
                           if customer['accountHolder']]
 | 
					                           if customer['accountHolder']]
 | 
				
			||||||
        if len(account_holders) > 1:
 | 
					        if account_holders:
 | 
				
			||||||
            log.warning("member %s has %s account holders in CORE: %s",
 | 
					            if len(account_holders) > 1:
 | 
				
			||||||
                        member['cardNo'], len(account_holders), member)
 | 
					                log.warning("member %s has %s account holders in CORE: %s",
 | 
				
			||||||
        elif not account_holders:
 | 
					                            member['cardNo'], len(account_holders), member)
 | 
				
			||||||
            raise NotImplementedError("TODO: how to handle member with no account holders?")
 | 
					            customer = account_holders[0]
 | 
				
			||||||
        customer = account_holders[0]
 | 
					        elif customers:
 | 
				
			||||||
 | 
					            if len(customers) > 1:
 | 
				
			||||||
 | 
					                log.warning("member %s has %s customers but no account holders: %s",
 | 
				
			||||||
 | 
					                            member['cardNo'], len(customers), member)
 | 
				
			||||||
 | 
					            customer = customers[0]
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            raise NotImplementedError("TODO: how to handle member with no customers?")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
            'id': member['customerAccountID'],
 | 
					            'id': member['customerAccountID'],
 | 
				
			||||||
            'number': member['cardNo'],
 | 
					            'number': int(member['cardNo']),
 | 
				
			||||||
            'name': normalize_full_name(customer['firstName'],
 | 
					            'name': normalize_full_name(customer['firstName'],
 | 
				
			||||||
                                        customer['lastName']),
 | 
					                                        customer['lastName']),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -167,12 +181,22 @@ class PersonImporter(FromCOREPOSAPI, corepos_importing.model.PersonImporter):
 | 
				
			||||||
        Return a list of Person data objects for the given Member.  This
 | 
					        Return a list of Person data objects for the given Member.  This
 | 
				
			||||||
        logic is split out separately so that datasync can leverage it too.
 | 
					        logic is split out separately so that datasync can leverage it too.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
					        customers = member['customers']
 | 
				
			||||||
        people = []
 | 
					        people = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # make sure we put the account holder first in the list!
 | 
					        # make sure account holder is listed first
 | 
				
			||||||
        customers = sorted(member['customers'],
 | 
					        account_holder = None
 | 
				
			||||||
                           key=lambda cust: 1 if cust['accountHolder'] else 0,
 | 
					        secondary = False
 | 
				
			||||||
                           reverse=True)
 | 
					        mixedup = False
 | 
				
			||||||
 | 
					        for customer in customers:
 | 
				
			||||||
 | 
					            if customer['accountHolder'] and not secondary:
 | 
				
			||||||
 | 
					                account_holder = customer
 | 
				
			||||||
 | 
					            elif not customer['accountHolder']:
 | 
				
			||||||
 | 
					                secondary = True
 | 
				
			||||||
 | 
					            elif customer['accountHolder'] and secondary:
 | 
				
			||||||
 | 
					                mixedup = True
 | 
				
			||||||
 | 
					        if mixedup:
 | 
				
			||||||
 | 
					            raise NotImplementedError("TODO: should re-sort the customers list for member {}".format(member['cardNo']))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for i, customer in enumerate(customers, 1):
 | 
					        for i, customer in enumerate(customers, 1):
 | 
				
			||||||
            person = dict(customer)
 | 
					            person = dict(customer)
 | 
				
			||||||
| 
						 | 
					@ -450,3 +474,81 @@ class ProductImporter(FromCOREPOSAPI, importing.model.ProductImporter):
 | 
				
			||||||
            'regular_price_multiple': 1 if price is not None else None,
 | 
					            'regular_price_multiple': 1 if price is not None else None,
 | 
				
			||||||
            'regular_price_type': self.enum.PRICE_TYPE_REGULAR if price is not None else None,
 | 
					            'regular_price_type': self.enum.PRICE_TYPE_REGULAR if price is not None else None,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class MemberImporter(FromCOREPOSAPI, importing.model.MemberImporter):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Importer for member data from CORE POS API.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    key = 'id'
 | 
				
			||||||
 | 
					    supported_fields = [
 | 
				
			||||||
 | 
					        'id',
 | 
				
			||||||
 | 
					        'customer_uuid',
 | 
				
			||||||
 | 
					        'joined',
 | 
				
			||||||
 | 
					        'withdrew',
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # TODO: should make this configurable
 | 
				
			||||||
 | 
					    member_status_codes = [
 | 
				
			||||||
 | 
					        'PC',
 | 
				
			||||||
 | 
					        'TERM',
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # TODO: should make this configurable
 | 
				
			||||||
 | 
					    non_member_status_codes = [
 | 
				
			||||||
 | 
					        'REG',
 | 
				
			||||||
 | 
					        'INACT',
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def setup(self):
 | 
				
			||||||
 | 
					        super(MemberImporter, self).setup()
 | 
				
			||||||
 | 
					        self.customers = self.cache_model(model.Customer, key='number')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_host_objects(self):
 | 
				
			||||||
 | 
					        return self.get_core_members()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_customer(self, number):
 | 
				
			||||||
 | 
					        if hasattr(self, 'customers'):
 | 
				
			||||||
 | 
					            return self.customers.get(number)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            return self.session.query(model.Customer)\
 | 
				
			||||||
 | 
					                               .filter(model.Customer.number == number)\
 | 
				
			||||||
 | 
					                               .one()
 | 
				
			||||||
 | 
					        except NoResultFound:
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def normalize_host_object(self, member):
 | 
				
			||||||
 | 
					        customer = self.get_customer(member['cardNo'])
 | 
				
			||||||
 | 
					        if not customer:
 | 
				
			||||||
 | 
					            log.warning("Rattail customer not found for cardNo %s: %s",
 | 
				
			||||||
 | 
					                        member['cardNo'], member)
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if member['memberStatus'] in self.non_member_status_codes:
 | 
				
			||||||
 | 
					            log.debug("skipping non-member %s with status '%s': %s",
 | 
				
			||||||
 | 
					                      member['memberStatus'], member['cardNo'], member)
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        if member['memberStatus'] not in self.member_status_codes:
 | 
				
			||||||
 | 
					            # note that we will still import this one! we don't skip it
 | 
				
			||||||
 | 
					            log.warning("unexpected status '%s' for member %s: %s",
 | 
				
			||||||
 | 
					                        member['memberStatus'], member['cardNo'], member)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        joined = None
 | 
				
			||||||
 | 
					        if member['startDate'] and member['startDate'] != '0000-00-00 00:00:00':
 | 
				
			||||||
 | 
					            joined = datetime.datetime.strptime(member['startDate'],
 | 
				
			||||||
 | 
					                                                '%Y-%m-%d %H:%M:%S')
 | 
				
			||||||
 | 
					            joined = joined.date()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        withdrew = None
 | 
				
			||||||
 | 
					        if member['endDate'] and member['endDate'] != '0000-00-00 00:00:00':
 | 
				
			||||||
 | 
					            withdrew = datetime.datetime.strptime(member['endDate'],
 | 
				
			||||||
 | 
					                                                  '%Y-%m-%d %H:%M:%S')
 | 
				
			||||||
 | 
					            withdrew = withdrew.date()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					            'id': str(member['cardNo']),
 | 
				
			||||||
 | 
					            'customer_uuid': customer.uuid,
 | 
				
			||||||
 | 
					            'joined': joined,
 | 
				
			||||||
 | 
					            'withdrew': withdrew,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue