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:
parent
6f03461114
commit
0298e63384
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
83
rattail_corepos/corepos/importing/rattail.py
Normal file
83
rattail_corepos/corepos/importing/rattail.py
Normal 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),
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue