From a57f29fe1a7524a0a0c0aea5a2c66cb8e2b4cca0 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Thu, 7 Sep 2023 17:49:28 -0500 Subject: [PATCH] Add support for importing MemberEquityPayment from CORE-POS DB SQL only, no API for now --- rattail_corepos/commands.py | 12 +- ...be118_add_memberequitypayment_extension.py | 57 ++++++++++ rattail_corepos/db/model/__init__.py | 3 +- rattail_corepos/db/model/people.py | 51 +++++++++ rattail_corepos/importing/corepos/api.py | 15 +-- rattail_corepos/importing/corepos/db.py | 104 ++++++++++++++++++ rattail_corepos/importing/model.py | 12 ++ rattail_corepos/importing/versions.py | 9 ++ 8 files changed, 248 insertions(+), 15 deletions(-) create mode 100644 rattail_corepos/db/alembic/versions/08d879bbe118_add_memberequitypayment_extension.py diff --git a/rattail_corepos/commands.py b/rattail_corepos/commands.py index 299844d..63a1f5e 100644 --- a/rattail_corepos/commands.py +++ b/rattail_corepos/commands.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2020 Lance Edgar +# Copyright © 2010-2023 Lance Edgar # # This file is part of Rattail. # @@ -55,7 +55,14 @@ class ImportCOREPOSDB(commands.ImportSubcommand): handler_key = 'to_rattail.from_corepos_db_office_op.import' 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', 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 " @@ -64,6 +71,7 @@ class ImportCOREPOSDB(commands.ImportSubcommand): def get_handler_kwargs(self, **kwargs): if 'args' in kwargs: + kwargs['corepos_dbtype'] = kwargs['args'].corepos_dbtype kwargs['corepos_dbkey'] = kwargs['args'].corepos_dbkey return kwargs diff --git a/rattail_corepos/db/alembic/versions/08d879bbe118_add_memberequitypayment_extension.py b/rattail_corepos/db/alembic/versions/08d879bbe118_add_memberequitypayment_extension.py new file mode 100644 index 0000000..88e07f3 --- /dev/null +++ b/rattail_corepos/db/alembic/versions/08d879bbe118_add_memberequitypayment_extension.py @@ -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') diff --git a/rattail_corepos/db/model/__init__.py b/rattail_corepos/db/model/__init__.py index 5fbae87..e536107 100644 --- a/rattail_corepos/db/model/__init__.py +++ b/rattail_corepos/db/model/__init__.py @@ -25,7 +25,8 @@ Database schema extensions for CORE-POS integration """ from .stores import CoreStore -from .people import CorePerson, CoreCustomer, CoreCustomerShopper, CoreMember +from .people import (CorePerson, CoreCustomer, CoreCustomerShopper, + CoreMember, CoreMemberEquityPayment) from .products import (CoreDepartment, CoreSubdepartment, CoreVendor, CoreProduct, CoreProductCost) diff --git a/rattail_corepos/db/model/people.py b/rattail_corepos/db/model/people.py index 6e6f6cf..14df996 100644 --- a/rattail_corepos/db/model/people.py +++ b/rattail_corepos/db/model/people.py @@ -178,3 +178,54 @@ class CoreMember(model.Base): return str(self.member) 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') diff --git a/rattail_corepos/importing/corepos/api.py b/rattail_corepos/importing/corepos/api.py index 835d9ce..a77f095 100644 --- a/rattail_corepos/importing/corepos/api.py +++ b/rattail_corepos/importing/corepos/api.py @@ -835,16 +835,7 @@ class MemberImporter(FromCOREPOSAPI, corepos_importing.model.MemberImporter): def normalize_host_object(self, member): card_number = member['cardNo'] customer = self.get_customer_by_number(card_number) - if not customer: - 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 + person = self.app.get_person(customer) if customer else None # TODO: at first i was *skipping* non-member status records, # but since CORE sort of assumes all customers are members, @@ -883,8 +874,8 @@ class MemberImporter(FromCOREPOSAPI, corepos_importing.model.MemberImporter): return { 'number': card_number, 'corepos_account_id': int(member['customerAccountID']), - 'customer_uuid': customer.uuid, - 'person_uuid': person.uuid, + 'customer_uuid': customer.uuid if customer else None, + 'person_uuid': person.uuid if person else None, 'membership_type_number': typeno, 'joined': joined, 'withdrew': withdrew, diff --git a/rattail_corepos/importing/corepos/db.py b/rattail_corepos/importing/corepos/db.py index d20bc02..b9d7a3b 100644 --- a/rattail_corepos/importing/corepos/db.py +++ b/rattail_corepos/importing/corepos/db.py @@ -25,19 +25,27 @@ CORE POS (DB) -> Rattail data importing """ import decimal +import logging from collections import OrderedDict +from sqlalchemy import orm + 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.gpc import GPC from rattail_corepos import importing as corepos_importing +log = logging.getLogger(__name__) + + class FromCOREPOSToRattail(importing.FromSQLAlchemyHandler, importing.ToRattailHandler): """ 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")' host_key = 'corepos_db_office_op' corepos_dbkey = 'default' @@ -47,6 +55,12 @@ class FromCOREPOSToRattail(importing.FromSQLAlchemyHandler, importing.ToRattailH return "CORE-POS (DB/{})".format(self.corepos_dbkey) 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]) def get_importers(self): @@ -55,8 +69,17 @@ class FromCOREPOSToRattail(importing.FromSQLAlchemyHandler, importing.ToRattailH importers['Department'] = DepartmentImporter importers['Subdepartment'] = SubdepartmentImporter importers['Product'] = ProductImporter + importers['MemberEquityPayment'] = MemberEquityPaymentImporter 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): """ @@ -228,3 +251,84 @@ class ProductImporter(FromCOREPOS, corepos_importing.model.ProductImporter): }) 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 diff --git a/rattail_corepos/importing/model.py b/rattail_corepos/importing/model.py index 36830af..a71d52d 100644 --- a/rattail_corepos/importing/model.py +++ b/rattail_corepos/importing/model.py @@ -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): extensions = { diff --git a/rattail_corepos/importing/versions.py b/rattail_corepos/importing/versions.py index 926b60a..3da175a 100644 --- a/rattail_corepos/importing/versions.py +++ b/rattail_corepos/importing/versions.py @@ -37,6 +37,7 @@ class CoreposVersionMixin(object): importers['CoreCustomer'] = CoreCustomerImporter importers['CoreCustomerShopper'] = CoreCustomerShopperImporter importers['CoreMember'] = CoreMemberImporter + importers['CoreMemberEquityPayment'] = CoreMemberEquityPaymentImporter importers['CoreStore'] = CoreStoreImporter importers['CoreDepartment'] = CoreDepartmentImporter importers['CoreSubdepartment'] = CoreSubdepartmentImporter @@ -76,6 +77,14 @@ class CoreMemberImporter(base.VersionImporter): 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): @property