diff --git a/rattail_corepos/trainwreck/__init__.py b/rattail_corepos/trainwreck/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rattail_corepos/trainwreck/commands.py b/rattail_corepos/trainwreck/commands.py new file mode 100644 index 0000000..f7bbbdc --- /dev/null +++ b/rattail_corepos/trainwreck/commands.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2021 Lance Edgar +# +# This file is part of Rattail. +# +# Rattail is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# Rattail. If not, see . +# +################################################################################ +""" +Rattail Commands +""" + +from rattail import commands + + +class ImportCore(commands.ImportSubcommand): + """ + Import data from CORE-POS "trans" DB + """ + name = 'import-corepos' + description = __doc__.strip() + handler_spec = 'rattail_corepos.trainwreck.importing.corepos:FromCoreToTrainwreck' diff --git a/rattail_corepos/trainwreck/importing/__init__.py b/rattail_corepos/trainwreck/importing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rattail_corepos/trainwreck/importing/corepos.py b/rattail_corepos/trainwreck/importing/corepos.py new file mode 100644 index 0000000..62e08b5 --- /dev/null +++ b/rattail_corepos/trainwreck/importing/corepos.py @@ -0,0 +1,250 @@ +# -*- coding: utf-8; -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2021 Lance Edgar +# +# This file is part of Rattail. +# +# Rattail is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# Rattail. If not, see . +# +################################################################################ +""" +CORE-POS -> Trainwreck data importing +""" + +from corepos.db.office_trans import Session as CoreTransSession, model as coretrans +from corepos.db.office_op import Session as CoreSession, model as corepos + +from rattail import importing +from rattail.util import OrderedDict +from rattail.time import localtime, make_utc +from rattail.trainwreck import importing as trainwreck_importing + + +class FromCoreToTrainwreck(importing.FromSQLAlchemyHandler, trainwreck_importing.ToTrainwreckHandler): + """ + Import data from CORE-POS into Trainwreck + """ + host_title = "CORE-POS" + corepos_dbkey = 'default' + + def make_host_session(self): + return CoreTransSession(bind=self.config.coretrans_engines[self.corepos_dbkey]) + + def get_importer_kwargs(self, key, **kwargs): + kwargs = super(FromCoreToTrainwreck, self).get_importer_kwargs(key, **kwargs) + kwargs.setdefault('core_op_session', self.core_op_session) + return kwargs + + def begin_host_transaction(self): + super(FromCoreToTrainwreck, self).begin_host_transaction() + + self.core_op_session = CoreSession(bind=self.config.corepos_engines[self.corepos_dbkey]) + + def rollback_host_transaction(self): + super(FromCoreToTrainwreck, self).rollback_host_transaction() + + self.core_op_session.rollback() + self.core_op_session.close() + self.core_op_session = None + + def commit_host_transaction(self): + super(FromCoreToTrainwreck, self).commit_host_transaction() + + self.core_op_session.commit() + self.core_op_session.close() + self.core_op_session = None + + def get_importers(self): + importers = OrderedDict() + importers['Transaction'] = TransactionImporter + importers['TransactionItem'] = TransactionItemImporter + return importers + + +class FromCore(importing.Importer): + """ + Base class for CORE importers. + """ + + def fetch_details(self): + # TODO: should figure out "which" txns apply to our date range first, + # w/ a rather "loose" query, to avoid issues when a txn spans multiple + # dates..? + return self.host_session.query(coretrans.TransactionDetail)\ + .filter(coretrans.TransactionDetail.date_time >= self.start_time)\ + .filter(coretrans.TransactionDetail.date_time < self.end_time)\ + .order_by(coretrans.TransactionDetail.register_number, + coretrans.TransactionDetail.transaction_number, + coretrans.TransactionDetail.store_row_id)\ + .all() + + def make_system_id(self, detail): + assert detail.employee_number + core_id = '-'.join([ + str(detail.employee_number), + str(detail.register_number), + str(detail.transaction_number), + ]) + return '|'.join([ + str(detail.store_id), + core_id, + ]) + + +class TransactionImporter(FromCore, trainwreck_importing.model.TransactionImporter): + """ + Import transaction data from CORE-POS + """ + key = ('system', 'system_id') + importing_from_system = 'corepos' + supported_fields = [ + 'system', + 'system_id', + 'terminal_id', + 'receipt_number', + 'start_time', + 'end_time', + 'cashier_id', + 'cashier_name', + 'customer_id', + 'customer_name', + 'subtotal', + 'total', + ] + + def setup(self): + super(TransactionImporter, self).setup() + + app = self.config.get_app() + self.people_handler = app.get_people_handler() + + if 'cashier_name' in self.fields: + self.corepos_employees = self.cache_model(corepos.Employee, + session=self.core_op_session, + key='number') + + if 'customer_name' in self.fields: + query = self.core_op_session.query(corepos.CustData)\ + .filter(corepos.CustData.person_number == 1) + self.corepos_customers = self.cache_model(corepos.CustData, + session=self.core_op_session, + query=query, + key='card_number') + + def get_host_objects(self): + details = self.fetch_details() + transactions = [] + current = {} + + def collect(detail, i): + receipt_number = str(detail.transaction_number) + + date_time = detail.date_time + if date_time: + date_time = localtime(self.config, date_time) + date_time = make_utc(date_time) + + if current and current['receipt_number'] != receipt_number: + transactions.append(dict(current)) + current.clear() + + if not current: + current.update({ + 'system': self.enum.TRAINWRECK_SYSTEM_COREPOS, + 'system_id': self.make_system_id(detail), + 'terminal_id': str(detail.register_number), + 'receipt_number': receipt_number, + 'cashier_id': str(detail.employee_number) if detail.employee_number else None, + 'customer_id': str(detail.card_number) if detail.card_number else None, + 'start_time': date_time, + 'end_time': date_time, + }) + + if detail.transaction_type == 'C': + if 'Subtotal' in detail.description: + current['subtotal'] = detail.unit_price + current['total'] = detail.unit_price + + if date_time: + current['end_time'] = date_time + + self.progress_loop(collect, details, + message="Collecting transaction data") + + # don't forget to add the last one! + if current: + transactions.append(current) + return transactions + + def normalize_host_object(self, txn): + + if 'cashier_name' in self.fields: + txn['cashier_name'] = None + if txn['cashier_id']: + employee = self.corepos_employees.get(int(txn['cashier_id'])) + if employee: + txn['cashier_name'] = self.people_handler.normalize_full_name( + employee.first_name, employee.last_name) + + if 'customer_name' in self.fields: + txn['customer_name'] = None + if txn['customer_id']: + custdata = self.corepos_customers.get(int(txn['customer_id'])) + if custdata: + txn['customer_name'] = self.people_handler.normalize_full_name( + custdata.first_name, custdata.last_name) + + return txn + + +class TransactionItemImporter(FromCore, trainwreck_importing.model.TransactionItemImporter): + """ + Import transaction item data from CORE-POS + """ + key = ('transaction_system_id', 'sequence') + importing_from_system = 'corepos' + supported_fields = [ + 'transaction_system_id', + 'sequence', + 'item_scancode', + 'department_number', + 'description', + 'unit_price', + 'unit_quantity', + 'total', + 'void', + ] + + def get_host_objects(self): + return self.fetch_details() + + def normalize_host_object(self, detail): + + # TODO: this needs to be a lot smarter. for now this "works" i guess + if detail.transaction_type != 'I': + return + + return { + 'transaction_system_id': self.make_system_id(detail), + 'sequence': detail.transaction_id, + 'item_scancode': detail.upc, + 'department_number': detail.department_number, + 'description': detail.description, + 'unit_price': detail.unit_price, + 'unit_quantity': detail.quantity, + 'total': detail.total, + 'void': bool(detail.voided), + } diff --git a/setup.py b/setup.py index 5959d18..5dcc8b4 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ ################################################################################ # # Rattail -- Retail Software Framework -# Copyright © 2010-2020 Lance Edgar +# Copyright © 2010-2021 Lance Edgar # # This file is part of Rattail. # @@ -118,6 +118,9 @@ setup( 'import-corepos-db = rattail_corepos.commands:ImportCOREPOSDB', ], + 'trainwreck.commands': [ + 'import-corepos = rattail_corepos.trainwreck.commands:ImportCore', + ], }, )