Add initial Rattail -> CORE-POS export logic

only allows "update" for Vendor model so far.  more to come after testing...
This commit is contained in:
Lance Edgar 2020-03-03 21:45:11 -06:00
parent 6f03461114
commit 0298e63384
5 changed files with 251 additions and 8 deletions

View file

@ -30,6 +30,23 @@ from rattail import commands
from rattail.util import load_object
class ExportCore(commands.ImportSubcommand):
"""
Export data from Rattail to CORE-POS
"""
name = 'export-corepos'
description = __doc__.strip()
default_handler_spec = 'rattail_corepos.corepos.importing.rattail:FromRattailToCore'
def get_handler_factory(self, **kwargs):
if self.config:
spec = self.config.get('rattail.exporting', 'corepos.handler',
default=self.default_handler_spec)
else:
spec = self.default_handler_spec
return load_object(spec)
class ImportCOREPOS(commands.ImportSubcommand):
"""
Import data from a CORE POS database

View file

@ -1,26 +1,76 @@
# -*- coding: utf-8; -*-
"""
CORE-POS model importers
.. warning::
As of this writing, most classes in this module are "direct DB" importers,
which will write directly to MySQL. They are meant to be used in dry-run
mode only, and/or for sample data import to a dev system etc. They are
*NOT* meant for production use, as they will completely bypass any CORE
business rules logic which may exist.
However some of the importers will be refactored, so they directly use the
CORE API for *writing* data (although they may continue to read directly
from the DB). The hope is that *all* existing importers can be refactored
to write via API instead of DB, so for now all importers are left intact
until they can be dealt with (hopefully refactored, but if not, then
removed).
"""
from __future__ import unicode_literals, absolute_import
from sqlalchemy.orm.exc import NoResultFound
from rattail import importing
from corepos.db import model as corepos
from corepos.trans.db import model as coretrans
from corepos.db.office_op import model as corepos
from corepos.db.office_trans import model as coretrans
from corepos.api import CoreWebAPI
class ToCore(importing.ToSQLAlchemy):
"""
Base class for all CORE (operational) model importers
Base class for all CORE "operational" model importers.
Note that this class inherits from
:class:`~rattail:rattail.importing.sqlalchemy.ToSQLAlchemy` even though our
goal is to *not* write directly to the CORE DB. However, (for now)
importers will need to override methods to ensure API is used instead,
where applicable. But even once all have been refactored to write via API,
we *still* may want to keep using ``ToSQLAlchemy`` for the sake of reading
"cached local" data. (May depend on how robust the API is.)
"""
# TODO: should we standardize on the 'id' primary key? (can we even?)
# key = 'id'
def setup(self):
self.establish_api()
def establish_api(self):
url = self.config.require('corepos.api', 'url')
self.api = CoreWebAPI(url)
# TODO: this looks an awful lot like it belongs in rattail proper
def get_single_local_object(self, key):
"""
Fetch a particular record from CORE, via SQLAlchemy. This is used by
the Rattail -> CORE datasync consumer.
"""
query = self.session.query(self.model_class)
for i, field in enumerate(self.key):
query = query.filter(getattr(self.model_class, field) == key[i])
for option in self.cache_query_options():
query = query.options(option)
try:
return query.one()
except NoResultFound:
pass
class ToCoreTrans(importing.ToSQLAlchemy):
pass
"""
Base class for all CORE "transaction" model importers
"""
########################################
@ -39,7 +89,36 @@ class SubdepartmentImporter(ToCore):
class VendorImporter(ToCore):
model_class = corepos.Vendor
key = 'id'
key = 'vendorID'
supported_fields = [
'vendorID',
'vendorName',
'vendorAbbreviation',
'discountRate',
]
# TODO: this importer is in a bit of an experimental state at the moment.
# we only allow "update" b/c it will use the API instead of direct DB
allow_create = False
allow_delete = False
def normalize_local_object(self, vendor):
return {
'vendorID': str(vendor.id),
'vendorName': vendor.name,
'vendorAbbreviation': vendor.abbreviation,
'discountRate': vendor.discount_rate,
}
def update_object(self, vendor, data, local_data=None):
"""
Push an update for the vendor, via the CORE API.
"""
if self.dry_run:
return data
vendorID = data.pop('vendorID')
self.api.set_vendor(vendorID, **data)
return data
class VendorContactImporter(ToCore):

View file

@ -0,0 +1,83 @@
# -*- coding: utf-8; -*-
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2020 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 <http://www.gnu.org/licenses/>.
#
################################################################################
"""
Rattail -> CORE-POS data export
"""
import logging
from rattail import importing
from rattail.db import model
from rattail.util import OrderedDict
from rattail_corepos.corepos import importing as corepos_importing
from rattail_corepos.corepos.importing.corepos import ToCoreHandler
log = logging.getLogger(__name__)
class FromRattailToCore(importing.FromRattailHandler, ToCoreHandler):
"""
Rattail -> CORE-POS export handler
"""
host_title = "Rattail"
local_title = "CORE-POS"
direction = 'export'
def get_importers(self):
importers = OrderedDict()
importers['Vendor'] = VendorImporter
return importers
class FromRattail(importing.FromSQLAlchemy):
"""
Base class for Rattail -> CORE-POS exporters.
"""
class VendorImporter(FromRattail, corepos_importing.model.VendorImporter):
"""
Vendor data exporter
"""
host_model_class = model.Vendor
key = 'vendorID'
supported_fields = [
'vendorID',
'vendorName',
'vendorAbbreviation',
'discountRate',
]
def normalize_host_object(self, vendor):
if not vendor.id or not vendor.id.isdigit():
log.warning("vendor %s has incompatible ID value: %s",
vendor.uuid, vendor.id)
return
return {
'vendorID': vendor.id,
'vendorName': vendor.name,
'vendorAbbreviation': vendor.abbreviation,
'discountRate': float(vendor.special_discount),
}

View file

@ -2,7 +2,7 @@
################################################################################
#
# Rattail -- Retail Software Framework
# Copyright © 2010-2018 Lance Edgar
# Copyright © 2010-2020 Lance Edgar
#
# This file is part of Rattail.
#
@ -27,7 +27,7 @@ DataSync for CORE POS
from corepos.db.office_op import Session as CoreSession, model as corepos
from rattail.db import model
from rattail.datasync import DataSyncWatcher
from rattail.datasync import DataSyncWatcher, NewDataSyncImportConsumer
class CoreOfficeOpWatcher(DataSyncWatcher):
@ -130,3 +130,66 @@ class COREPOSProductWatcher(DataSyncWatcher):
session.close()
return changes
class FromRattailToCore(NewDataSyncImportConsumer):
"""
Rattail -> CORE POS datasync consumer
"""
handler_spec = 'rattail_corepos.corepos.importing.rattail:FromRattailToCore'
def begin_transaction(self):
self.corepos_session = CoreSession()
def rollback_transaction(self):
self.corepos_session.rollback()
self.corepos_session.close()
def commit_transaction(self):
self.corepos_session.commit()
self.corepos_session.close()
def process_changes(self, session, changes):
"""
Process all the given changes, coming from Rattail.
"""
# TODO: this probably doesn't accomplish anything here?
if self.runas_username:
session.set_continuum_user(self.runas_username)
# update all importers with current Rattail/CORE sessions
for importer in self.importers.values():
importer.host_session = session
importer.session = self.corepos_session
# also establish the API client for each!
importer.establish_api()
# next pass syncs all Vendor changes
types = [
'Vendor',
'VendorPhoneNumber',
'VendorEmailAddress',
]
for change in [c for c in changes if c.payload_type in types]:
vendor = self.get_host_vendor(session, change)
if vendor:
# TODO: what about "deletions" - not sure what happens yet
self.process_change(session, self.importers['Vendor'],
host_object=vendor)
# self.process_change(session, self.importers['VendorContact'],
# host_object=vendor)
def get_host_vendor(self, session, change):
if change.payload_type == 'Vendor':
return session.query(model.Vendor).get(change.payload_key)
if change.payload_type == 'VendorPhoneNumber':
phone = session.query(model.VendorPhoneNumber).get(change.payload_key)
if phone:
return phone.vendor
if change.payload_type == 'VendorEmailAddress':
email = session.query(model.VendorEmailAddress).get(change.payload_key)
if email:
return email.vendor

View file

@ -112,6 +112,7 @@ setup(
],
'rattail.commands': [
'export-corepos = rattail_corepos.commands:ExportCore',
'corepos-import-square = rattail_corepos.commands:CoreImportSquare',
'import-corepos = rattail_corepos.commands:ImportCOREPOS',
],