diff --git a/rattail_corepos/commands.py b/rattail_corepos/commands.py index 1a2f7dd..ba5c234 100644 --- a/rattail_corepos/commands.py +++ b/rattail_corepos/commands.py @@ -54,3 +54,12 @@ class ImportCOREPOS(commands.ImportSubcommand): if 'args' in kwargs: kwargs['corepos_dbkey'] = kwargs['args'].corepos_dbkey return kwargs + + +class CoreImportSquare(commands.ImportFromCSV): + """ + Import transaction data from Square into CORE + """ + name = 'corepos-import-square' + description = __doc__.strip() + handler_spec = 'rattail_corepos.corepos.importing.square:FromSquareToCoreTrans' diff --git a/rattail_corepos/config.py b/rattail_corepos/config.py index c27fc9d..d7d7666 100644 --- a/rattail_corepos/config.py +++ b/rattail_corepos/config.py @@ -37,9 +37,15 @@ class RattailCOREPOSExtension(ConfigExtension): key = 'rattail-corepos' def configure(self, config): - from rattail_corepos.db import Session + from corepos.db import Session as CoreSession + from corepos.trans.db import Session as CoreTransSession engines = get_engines(config, section='rattail_corepos.db') config.corepos_engines = engines config.corepos_engine = engines.get('default') - Session.configure(bind=config.corepos_engine) + CoreSession.configure(bind=config.corepos_engine) + + engines = get_engines(config, section='rattail_coretrans.db') + config.coretrans_engines = engines + config.coretrans_engine = engines.get('default') + CoreTransSession.configure(bind=config.coretrans_engine) diff --git a/rattail_corepos/corepos/__init__.py b/rattail_corepos/corepos/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rattail_corepos/corepos/importing/__init__.py b/rattail_corepos/corepos/importing/__init__.py new file mode 100644 index 0000000..62949e3 --- /dev/null +++ b/rattail_corepos/corepos/importing/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8; -*- +""" +Importing data into CORE-POS +""" + +from __future__ import unicode_literals, absolute_import + +from . import model diff --git a/rattail_corepos/corepos/importing/model.py b/rattail_corepos/corepos/importing/model.py new file mode 100644 index 0000000..08fad0f --- /dev/null +++ b/rattail_corepos/corepos/importing/model.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8; -*- +""" +CORE-POS model importers +""" + +from __future__ import unicode_literals, absolute_import + +from rattail import importing + +from corepos.db import model as corepos +from corepos.trans.db import model as coretrans + + +class ToCore(importing.ToSQLAlchemy): + pass + + +class ToCoreTrans(importing.ToSQLAlchemy): + pass + + +class CustomerImporter(ToCore): + """ + CORE-POS customer data importer. + """ + model_class = corepos.Customer + + +class TransactionDetailImporter(ToCoreTrans): + """ + CORE-POS transaction data importer. + """ + model_class = coretrans.TransactionDetail diff --git a/rattail_corepos/corepos/importing/square.py b/rattail_corepos/corepos/importing/square.py new file mode 100644 index 0000000..16e7baa --- /dev/null +++ b/rattail_corepos/corepos/importing/square.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8; -*- +""" +Square -> CORE-POS data importing +""" + +from __future__ import unicode_literals, absolute_import + +import re +import datetime +import decimal + +import six +import sqlalchemy as sa + +from corepos.trans.db import Session as CoreTransSession, model as coretrans + +from rattail import importing +from rattail.util import OrderedDict +from rattail_corepos.corepos import importing as corepos_importing + + +class FromSquareToCoreTrans(importing.ToSQLAlchemyHandler): + """ + Square -> CORE-POS import handler. + """ + host_title = "Square" + local_title = "CORE-POS" + + def make_session(self): + return CoreTransSession() + + def get_importers(self): + importers = OrderedDict() + importers['TransactionDetail'] = TransactionDetailImporter + return importers + + +class FromSquare(importing.FromCSV): + """ + Base class for Square -> CORE-POS importers. + """ + + +class TransactionDetailImporter(FromSquare, corepos_importing.model.TransactionDetailImporter): + """ + Transaction detail importer. + """ + key = 'store_row_id' + supported_fields = [ + 'store_row_id', + 'date_time', + 'card_number', + 'upc', + 'description', + 'quantity', + 'unit_price', + 'discount', + 'tax', + 'total', + ] + + def setup(self): + super(TransactionDetailImporter, self).setup() + + # cache existing transactions by ID + self.transaction_details = self.cache_model(coretrans.TransactionDetail, + key=self.transaction_detail_key) + + # keep track of new IDs + self.new_ids = {} + self.last_new_id = self.get_last_new_id() + + def transaction_detail_key(self, detail, normal): + return ( + detail.store_id, + detail.register_number, + detail.date_time, + detail.upc, + ) + + def get_last_new_id(self): + # TODO: pretty sure there is a better way to do this... + return self.session.query(sa.func.max(coretrans.TransactionDetail.store_row_id))\ + .scalar() or 0 + + currency_pattern = re.compile(r'^\$(?P\d+\.\d\d)$') + currency_pattern_negative = re.compile(r'^\(\$(?P\d+\.\d\d)\)$') + + def parse_currency(self, value): + value = (value or '').strip() or None + if value: + + # first check for positive amount + match = self.currency_pattern.match(value) + if match: + return float(match.group('amount')) + + # okay then, check for negative amount + match = self.currency_pattern_negative.match(value) + if match: + return 0 - float(match.group('amount')) + + def normalize_host_object(self, csvrow): + + # date_time + date = datetime.datetime.strptime(csvrow['Date'], '%m/%d/%Y').date() + time = datetime.datetime.strptime(csvrow['Time'], '%H:%M:%S').time() + date_time = datetime.datetime.combine(date, time) + + # upc + upc = csvrow['SKU'] + + # store_row_id + key = ( + 0, # store_id + None, # register_number + date_time, + upc, + ) + if key in self.transaction_details: + store_row_id = self.transaction_details[key].store_row_id + else: + store_row_id = self.last_new_id + 1 + self.new_ids[store_row_id] = csvrow + self.last_new_id = store_row_id + + # card_number + card_number = csvrow['Customer Reference ID'] or None + if card_number: + card_number = int(card_number) + + # quantity + quantity = float(csvrow['Qty']) + + # unit_price + unit_price = self.parse_currency(csvrow['Gross Sales']) + if unit_price is not None: + unit_price /= quantity + unit_price = decimal.Decimal('{:0.2f}'.format(unit_price)) + elif csvrow['Gross Sales']: + log.warning("cannot parse 'unit_price' from: %s", csvrow['Gross Sales']) + + # discount + discount = self.parse_currency(csvrow['Discounts']) + if discount is not None: + discount = decimal.Decimal('{:0.2f}'.format(discount)) + elif csvrow['Discounts']: + log.warning("cannot parse 'discount' from: %s", csvrow['Discounts']) + + # tax + tax = self.parse_currency(csvrow['Tax']) + if csvrow['Tax'] and tax is None: + log.warning("cannot parse 'tax' from: %s", csvrow['Tax']) + tax = bool(tax) + + # total + total = self.parse_currency(csvrow['Net Sales']) + if total is not None: + total = decimal.Decimal('{:0.2f}'.format(total)) + elif csvrow['Net Sales']: + log.warning("cannot parse 'total' from: %s", csvrow['Net Sales']) + + return { + 'store_row_id': store_row_id, + 'date_time': date_time, + 'card_number': card_number, + 'upc': upc, + 'description': csvrow['Item'], + 'quantity': quantity, + 'unit_price': unit_price, + 'discount': discount, + 'tax': tax, + 'total': total, + } diff --git a/setup.py b/setup.py index 1463138..493b798 100644 --- a/setup.py +++ b/setup.py @@ -104,6 +104,7 @@ setup( ], 'rattail.commands': [ + 'corepos-import-square = rattail_corepos.commands:CoreImportSquare', 'import-corepos = rattail_corepos.commands:ImportCOREPOS', ],