Add support for importing MemberEquityPayment from CORE-POS DB

SQL only, no API for now
This commit is contained in:
Lance Edgar 2023-09-07 17:49:28 -05:00
parent 35e24422a2
commit a57f29fe1a
8 changed files with 248 additions and 15 deletions

View file

@ -2,7 +2,7 @@
################################################################################ ################################################################################
# #
# Rattail -- Retail Software Framework # Rattail -- Retail Software Framework
# Copyright © 2010-2020 Lance Edgar # Copyright © 2010-2023 Lance Edgar
# #
# This file is part of Rattail. # This file is part of Rattail.
# #
@ -55,7 +55,14 @@ class ImportCOREPOSDB(commands.ImportSubcommand):
handler_key = 'to_rattail.from_corepos_db_office_op.import' handler_key = 'to_rattail.from_corepos_db_office_op.import'
def add_parser_args(self, parser): def add_parser_args(self, parser):
super(ImportCOREPOSDB, self).add_parser_args(parser) super().add_parser_args(parser)
parser.add_argument('--corepos-dbtype', metavar='TYPE', default='office_op',
choices=['office_op', 'office_trans'],
help="Config *type* for CORE-POS database engine to be used as "
"host. Default type is 'office_op' - this determines which "
"config section is used with regard to the --corepos-dbkey arg.")
parser.add_argument('--corepos-dbkey', metavar='KEY', default='default', parser.add_argument('--corepos-dbkey', metavar='KEY', default='default',
help="Config key for CORE POS database engine to be used as the \"host\", " help="Config key for CORE POS database engine to be used as the \"host\", "
"i.e. the source of the data to be imported. This key " "must be " "i.e. the source of the data to be imported. This key " "must be "
@ -64,6 +71,7 @@ class ImportCOREPOSDB(commands.ImportSubcommand):
def get_handler_kwargs(self, **kwargs): def get_handler_kwargs(self, **kwargs):
if 'args' in kwargs: if 'args' in kwargs:
kwargs['corepos_dbtype'] = kwargs['args'].corepos_dbtype
kwargs['corepos_dbkey'] = kwargs['args'].corepos_dbkey kwargs['corepos_dbkey'] = kwargs['args'].corepos_dbkey
return kwargs return kwargs

View file

@ -0,0 +1,57 @@
# -*- coding: utf-8; -*-
"""add MemberEquityPayment extension
Revision ID: 08d879bbe118
Revises: b025df7cf41b
Create Date: 2023-09-06 17:44:43.874500
"""
# revision identifiers, used by Alembic.
revision = '08d879bbe118'
down_revision = 'b025df7cf41b'
branch_labels = None
depends_on = None
from alembic import op
import sqlalchemy as sa
import rattail.db.types
def upgrade():
# corepos_member_equity_payment
op.create_table('corepos_member_equity_payment',
sa.Column('uuid', sa.String(length=32), nullable=False),
sa.Column('corepos_card_number', sa.Integer(), nullable=False),
sa.Column('corepos_transaction_number', sa.String(length=50), nullable=True),
sa.Column('corepos_transaction_id', sa.Integer(), nullable=True),
sa.Column('corepos_department_number', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['uuid'], ['member_equity_payment.uuid'], name='corepos_member_equity_payment_fk_payment'),
sa.PrimaryKeyConstraint('uuid')
)
op.create_table('corepos_member_equity_payment_version',
sa.Column('uuid', sa.String(length=32), autoincrement=False, nullable=False),
sa.Column('corepos_card_number', sa.Integer(), autoincrement=False, nullable=True),
sa.Column('corepos_transaction_number', sa.String(length=50), autoincrement=False, nullable=True),
sa.Column('corepos_transaction_id', sa.Integer(), autoincrement=False, nullable=True),
sa.Column('corepos_department_number', sa.Integer(), autoincrement=False, nullable=True),
sa.Column('transaction_id', sa.BigInteger(), autoincrement=False, nullable=False),
sa.Column('end_transaction_id', sa.BigInteger(), nullable=True),
sa.Column('operation_type', sa.SmallInteger(), nullable=False),
sa.PrimaryKeyConstraint('uuid', 'transaction_id')
)
op.create_index(op.f('ix_corepos_member_equity_payment_version_end_transaction_id'), 'corepos_member_equity_payment_version', ['end_transaction_id'], unique=False)
op.create_index(op.f('ix_corepos_member_equity_payment_version_operation_type'), 'corepos_member_equity_payment_version', ['operation_type'], unique=False)
op.create_index(op.f('ix_corepos_member_equity_payment_version_transaction_id'), 'corepos_member_equity_payment_version', ['transaction_id'], unique=False)
def downgrade():
# corepos_member_equity_payment
op.drop_index(op.f('ix_corepos_member_equity_payment_version_transaction_id'), table_name='corepos_member_equity_payment_version')
op.drop_index(op.f('ix_corepos_member_equity_payment_version_operation_type'), table_name='corepos_member_equity_payment_version')
op.drop_index(op.f('ix_corepos_member_equity_payment_version_end_transaction_id'), table_name='corepos_member_equity_payment_version')
op.drop_table('corepos_member_equity_payment_version')
op.drop_table('corepos_member_equity_payment')

View file

@ -25,7 +25,8 @@ Database schema extensions for CORE-POS integration
""" """
from .stores import CoreStore from .stores import CoreStore
from .people import CorePerson, CoreCustomer, CoreCustomerShopper, CoreMember from .people import (CorePerson, CoreCustomer, CoreCustomerShopper,
CoreMember, CoreMemberEquityPayment)
from .products import (CoreDepartment, CoreSubdepartment, from .products import (CoreDepartment, CoreSubdepartment,
CoreVendor, CoreProduct, CoreProductCost) CoreVendor, CoreProduct, CoreProductCost)

View file

@ -178,3 +178,54 @@ class CoreMember(model.Base):
return str(self.member) return str(self.member)
CoreMember.make_proxy(model.Member, '_corepos', 'corepos_account_id') CoreMember.make_proxy(model.Member, '_corepos', 'corepos_account_id')
class CoreMemberEquityPayment(model.Base):
"""
CORE-specific extensions to
:class:`~rattail:rattail.db.model.MemberEquityPayment`.
"""
__tablename__ = 'corepos_member_equity_payment'
__table_args__ = (
sa.ForeignKeyConstraint(['uuid'], ['member_equity_payment.uuid'],
name='corepos_member_equity_payment_fk_payment'),
)
__versioned__ = {}
uuid = model.uuid_column(default=None)
payment = orm.relationship(
model.MemberEquityPayment,
doc="""
Reference to the actual payment record, which this one extends.
""",
backref=orm.backref(
'_corepos',
uselist=False,
cascade='all, delete-orphan',
doc="""
Reference to the CORE-POS extension record for this payment.
"""))
corepos_card_number = sa.Column(sa.Integer(), nullable=False, doc="""
``stockpurchases.card_no`` value for this payment, within CORE-POS.
""")
corepos_transaction_number = sa.Column(sa.String(length=50), nullable=True, doc="""
``stockpurchases.trans_num`` value for this payment, within CORE-POS.
""")
corepos_transaction_id = sa.Column(sa.Integer(), nullable=True, doc="""
``stockpurchases.trans_id`` value for this payment, within CORE-POS.
""")
corepos_department_number = sa.Column(sa.Integer(), nullable=True, doc="""
``stockpurchases.dept`` value for this payment, within CORE-POS.
""")
def __str__(self):
return str(self.payment)
CoreMemberEquityPayment.make_proxy(model.MemberEquityPayment, '_corepos', 'corepos_card_number')
CoreMemberEquityPayment.make_proxy(model.MemberEquityPayment, '_corepos', 'corepos_transaction_number')
CoreMemberEquityPayment.make_proxy(model.MemberEquityPayment, '_corepos', 'corepos_transaction_id')
CoreMemberEquityPayment.make_proxy(model.MemberEquityPayment, '_corepos', 'corepos_department_number')

View file

@ -835,16 +835,7 @@ class MemberImporter(FromCOREPOSAPI, corepos_importing.model.MemberImporter):
def normalize_host_object(self, member): def normalize_host_object(self, member):
card_number = member['cardNo'] card_number = member['cardNo']
customer = self.get_customer_by_number(card_number) customer = self.get_customer_by_number(card_number)
if not customer: person = self.app.get_person(customer) if customer else None
log.warning("Rattail customer not found for cardNo %s: %s",
card_number, member)
return
person = self.app.get_person(customer)
if not person:
log.warning("Rattail person not found for cardNo %s: %s",
card_number, member)
return
# TODO: at first i was *skipping* non-member status records, # TODO: at first i was *skipping* non-member status records,
# but since CORE sort of assumes all customers are members, # but since CORE sort of assumes all customers are members,
@ -883,8 +874,8 @@ class MemberImporter(FromCOREPOSAPI, corepos_importing.model.MemberImporter):
return { return {
'number': card_number, 'number': card_number,
'corepos_account_id': int(member['customerAccountID']), 'corepos_account_id': int(member['customerAccountID']),
'customer_uuid': customer.uuid, 'customer_uuid': customer.uuid if customer else None,
'person_uuid': person.uuid, 'person_uuid': person.uuid if person else None,
'membership_type_number': typeno, 'membership_type_number': typeno,
'joined': joined, 'joined': joined,
'withdrew': withdrew, 'withdrew': withdrew,

View file

@ -25,19 +25,27 @@ CORE POS (DB) -> Rattail data importing
""" """
import decimal import decimal
import logging
from collections import OrderedDict from collections import OrderedDict
from sqlalchemy import orm
from corepos.db.office_op import model as corepos, Session as CoreSession from corepos.db.office_op import model as corepos, Session as CoreSession
from corepos.db.office_trans import model as coretrans, Session as CoreTransSession
from rattail import importing from rattail import importing
from rattail.gpc import GPC from rattail.gpc import GPC
from rattail_corepos import importing as corepos_importing from rattail_corepos import importing as corepos_importing
log = logging.getLogger(__name__)
class FromCOREPOSToRattail(importing.FromSQLAlchemyHandler, importing.ToRattailHandler): class FromCOREPOSToRattail(importing.FromSQLAlchemyHandler, importing.ToRattailHandler):
""" """
Import handler for data coming from a CORE POS database. Import handler for data coming from a CORE POS database.
""" """
# TODO: these should be changed, it now allows for "trans" DB too..
generic_host_title = 'CORE Office (DB "op")' generic_host_title = 'CORE Office (DB "op")'
host_key = 'corepos_db_office_op' host_key = 'corepos_db_office_op'
corepos_dbkey = 'default' corepos_dbkey = 'default'
@ -47,6 +55,12 @@ class FromCOREPOSToRattail(importing.FromSQLAlchemyHandler, importing.ToRattailH
return "CORE-POS (DB/{})".format(self.corepos_dbkey) return "CORE-POS (DB/{})".format(self.corepos_dbkey)
def make_host_session(self): def make_host_session(self):
# session type depends on the --corepos-dbtype arg
if self.corepos_dbtype == 'office_trans':
return CoreTransSession(bind=self.config.coretrans_engines[self.corepos_dbkey])
# assume office_op by default
return CoreSession(bind=self.config.corepos_engines[self.corepos_dbkey]) return CoreSession(bind=self.config.corepos_engines[self.corepos_dbkey])
def get_importers(self): def get_importers(self):
@ -55,8 +69,17 @@ class FromCOREPOSToRattail(importing.FromSQLAlchemyHandler, importing.ToRattailH
importers['Department'] = DepartmentImporter importers['Department'] = DepartmentImporter
importers['Subdepartment'] = SubdepartmentImporter importers['Subdepartment'] = SubdepartmentImporter
importers['Product'] = ProductImporter importers['Product'] = ProductImporter
importers['MemberEquityPayment'] = MemberEquityPaymentImporter
return importers return importers
def get_default_keys(self):
keys = super().get_default_keys()
if 'MemberEquityPayment' in keys:
keys.remove('MemberEquityPayment')
return keys
class FromCOREPOS(importing.FromSQLAlchemy): class FromCOREPOS(importing.FromSQLAlchemy):
""" """
@ -228,3 +251,84 @@ class ProductImporter(FromCOREPOS, corepos_importing.model.ProductImporter):
}) })
return data return data
class MemberEquityPaymentImporter(FromCOREPOS, corepos_importing.model.MemberEquityPaymentImporter):
"""
Imports equity payment data from CORE-POS
"""
host_model_class = coretrans.StockPurchase
# TODO: this is composite key for StockPurchase, but may need to change?
key = ('member_uuid', 'received', 'transaction_identifier')
supported_fields = [
'member_uuid',
'amount',
'received',
'transaction_identifier',
'corepos_card_number',
'corepos_transaction_number',
'corepos_transaction_id',
'corepos_department_number',
]
def setup(self):
super().setup()
model = self.model
query = self.session.query(model.Member)\
.join(model.Customer)\
.join(model.CoreCustomer)\
.options(orm.joinedload(model.Member.customer)\
.joinedload(model.Customer._corepos))
key = lambda member, normal: member.customer.corepos_card_number
self.members_by_card_number = self.cache_model(model.Member,
query=query,
key=key)
def get_member(self, card_number):
if hasattr(self, 'members_by_card_number'):
return self.members_by_card_number.get(card_number)
model = self.model
try:
return self.session.query(model.Member)\
.join(model.Customer)\
.join(model.CoreCustomer)\
.filter(model.CoreCustomer.corepos_card_number == card_number)\
.one()
except orm.exc.NoResultFound:
pass
def normalize_host_object(self, stock_purchase):
card_number = stock_purchase.card_number
member = self.get_member(card_number)
if not member:
log.warning("member not found for card number %s: %s",
card_number, stock_purchase)
return
received = stock_purchase.datetime
if received:
received = self.app.localtime(received)
received = self.app.make_utc(received)
return {
'member_uuid': member.uuid,
'amount': stock_purchase.amount,
'received': received,
'transaction_identifier': stock_purchase.transaction_number,
'corepos_card_number': stock_purchase.card_number,
'corepos_transaction_number': stock_purchase.transaction_number,
'corepos_transaction_id': stock_purchase.transaction_id,
'corepos_department_number': stock_purchase.department_number,
}
def create_object(self, key, host_data):
payment = super().create_object(key, host_data)
if payment:
# track where each payment comes from!
payment.source = 'corepos'
return payment

View file

@ -82,6 +82,18 @@ class MemberImporter(importing.model.MemberImporter):
} }
class MemberEquityPaymentImporter(importing.model.MemberEquityPaymentImporter):
extensions = {
'_corepos': [
'corepos_card_number',
'corepos_transaction_number',
'corepos_transaction_id',
'corepos_department_number',
],
}
class StoreImporter(importing.model.StoreImporter): class StoreImporter(importing.model.StoreImporter):
extensions = { extensions = {

View file

@ -37,6 +37,7 @@ class CoreposVersionMixin(object):
importers['CoreCustomer'] = CoreCustomerImporter importers['CoreCustomer'] = CoreCustomerImporter
importers['CoreCustomerShopper'] = CoreCustomerShopperImporter importers['CoreCustomerShopper'] = CoreCustomerShopperImporter
importers['CoreMember'] = CoreMemberImporter importers['CoreMember'] = CoreMemberImporter
importers['CoreMemberEquityPayment'] = CoreMemberEquityPaymentImporter
importers['CoreStore'] = CoreStoreImporter importers['CoreStore'] = CoreStoreImporter
importers['CoreDepartment'] = CoreDepartmentImporter importers['CoreDepartment'] = CoreDepartmentImporter
importers['CoreSubdepartment'] = CoreSubdepartmentImporter importers['CoreSubdepartment'] = CoreSubdepartmentImporter
@ -76,6 +77,14 @@ class CoreMemberImporter(base.VersionImporter):
return model.CoreMember return model.CoreMember
class CoreMemberEquityPaymentImporter(base.VersionImporter):
@property
def host_model_class(self):
model = self.config.get_model()
return model.CoreMemberEquityPayment
class CoreStoreImporter(base.VersionImporter): class CoreStoreImporter(base.VersionImporter):
@property @property