Add 2-way sync for basic Member data, CORE <-> Rattail

This commit is contained in:
Lance Edgar 2020-03-18 11:30:18 -05:00
parent 9dbdb81f07
commit 8917316a21
5 changed files with 158 additions and 14 deletions

View file

@ -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

View file

@ -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,
} }

View file

@ -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):

View file

@ -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 = [

View file

@ -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 account_holders:
if len(account_holders) > 1: if len(account_holders) > 1:
log.warning("member %s has %s account holders in CORE: %s", log.warning("member %s has %s account holders in CORE: %s",
member['cardNo'], len(account_holders), member) member['cardNo'], len(account_holders), member)
elif not account_holders:
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,
}