Make Odoo as a driver of ERP

This feature is trying to make the back end ERP driver plugable. As
a result, the current interaction with Odoo will be capsulated as
an ERP driver.

Implement blueprint: erp-driver

Change-Id: I58e1a3d1f47806b8ffeeb7244fe8806683efaced
This commit is contained in:
Fei Long Wang 2017-01-20 15:11:15 +13:00
parent 6d1dec6cd9
commit 5077347972
16 changed files with 347 additions and 10 deletions

View File

@ -38,6 +38,10 @@ DEFAULT_OPTIONS = (
default=[],
help=('The tenant name list which will be ignored when '
'collecting metrics from Ceilometer.')),
cfg.StrOpt('erp_driver',
default='odoo',
help='The ERP driver used for Distil',
),
)
COLLECTOR_OPTS = [

0
distil/erp/__init__.py Normal file
View File

66
distil/erp/driver.py Normal file
View File

@ -0,0 +1,66 @@
# Copyright 2017 Catalyst IT Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
class BaseDriver(object):
"""Base class for ERP drivers.
"""
conf = None
def __init__(self, conf):
self.conf = conf
def get_salesOrders(self, project, start_at, end_at):
"""List sales orders based on the given project and time range
:param project: project id
:param start_at: start time
:param end_at: end time
:returns List of sales order, if the time range only cover one month,
then, the list will only contain 1 sale orders. Otherwise,
the length of the list depends on the months number of the
time range.
"""
raise NotImplementedError()
def get_products(self, regions=None):
"""List products based o given regions
:param regions: List of regions to get projects
:returns Dict of products based on the given regions
"""
raise NotImplementedError()
def create_product(self, product):
"""Create product in Odoo.
:param product: info used to create product
"""
raise NotImplementedError()
def get_credits(self, project):
"""Get project credits
:param instance: nova.objects.instance.Instance
:returns list of credits current project can get
"""
raise NotImplementedError()
def create_credit(self, project, credit):
"""Create credit for a given project
:param project: project
"""
raise NotImplementedError()

View File

View File

@ -0,0 +1,97 @@
# Copyright (c) 2016 Catalyst IT Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import odoorpc
from oslo_log import log
from distil.erp import driver
from distil.common import openstack
LOG = log.getLogger(__name__)
PRODUCT_CATEGORY = ('Compute', 'Network', 'Block Storage', 'Object Storage')
class OdooDriver(driver.BaseDriver):
def __init__(self, conf):
self.odoo = odoorpc.ODOO(conf.odoo.hostname,
protocol=conf.odoo.protocol,
port=conf.odoo.port,
version=conf.odoo.version)
self.odoo.login(conf.odoo.database, conf.odoo.user, conf.odoo.password)
# NOTE(flwang): This is not necessary for most of cases, but just in
# case some cloud providers are using different region name formats in
# Keystone and Odoo.
if conf.odoo.region_mapping:
regions = conf.odoo.region_mapping.split(',')
self.region_mapping = dict([(r.split(":")[0].strip(),
r.split(":")[1].strip())
for r in regions])
self.order = self.odoo.env['sale.order']
self.orderline = self.odoo.env['sale.order.line']
self.tenant = self.odoo.env['cloud.tenant']
self.partner = self.odoo.env['res.partner']
self.pricelist = self.odoo.env['product.pricelist']
self.product = self.odoo.env['product.product']
self.category = self.odoo.env['product.category']
def get_products(self, regions=None):
if not regions:
regions = [r.id for r in openstack.get_regions()]
if hasattr(self, 'region_mapping'):
regions = self.region_mapping.values()
else:
if hasattr(self, 'region_mapping'):
regions = [self.region_mapping.get(r) for r in regions]
prices = {}
try:
for r in regions:
if not r:
continue
prices[r] = {}
for cat in PRODUCT_CATEGORY:
prices[r][cat.lower()] = []
c = self.category.search([('name', '=', cat),
('display_name', 'ilike', r)])
product_ids = self.product.search([('categ_id', '=', c[0]),
('sale_ok', '=', True),
('active', '=', True)])
products = self.odoo.execute('product.product',
'read',
product_ids)
for p in products:
name = p['name_template'][len(r) + 1:]
if 'pre-prod' in name:
continue
price = round(p['lst_price'], 5)
# NOTE(flwang): default_code is Internal Reference on
# Odoo GUI
unit = p['default_code']
desc = p['description']
prices[r][cat.lower()].append({'resource': name,
'price': price,
'unit': unit,
'description': desc})
except odoorpc.error.Error as e:
LOG.exception(e)
return {}
return prices

47
distil/erp/utils.py Normal file
View File

@ -0,0 +1,47 @@
# Copyright (c) 2016 Catalyst IT Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy
# of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
import copy
import six
from oslo_log import log
from stevedore import driver
from distil import exceptions
LOG = log.getLogger(__name__)
def load_erp_driver(conf):
"""Loads a erp driver and returns it.
:param conf: Configuration instance to use for loading the
driver. Must include a 'drivers' group.
"""
_invoke_args = [conf]
try:
mgr = driver.DriverManager('distil.erp',
conf.erp_driver,
invoke_on_load=True,
invoke_args=_invoke_args)
return mgr.driver
except Exception as exc:
LOG.exception(exc)
raise exceptions.InvalidDriver('Failed to load ERP driver'
' for {0}'.format(conf.erp_driver))

View File

@ -75,3 +75,8 @@ class DateTimeException(DistilException):
class Forbidden(DistilException):
code = 403
message = _("You are not authorized to complete this action")
class InvalidDriver(DistilException):
"""A driver was not found or loaded."""
message = _("Failed to load driver")

View File

@ -15,13 +15,13 @@
from distil import rater
from distil.rater import rate_file
from distil.common import odoo
from distil.service.api.v2 import products
class OdooRater(rater.BaseRater):
def __init__(self):
self.prices = odoo.Odoo().get_prices()
self.prices = products.get_products()
def rate(self, name, region=None):
if not self.prices:

View File

@ -15,9 +15,9 @@
from oslo_config import cfg
from oslo_log import log as logging
from distil.common import odoo
from distil.common import cache
from distil.erp import utils as erp_utils
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
@ -25,4 +25,6 @@ CONF = cfg.CONF
@cache.memoize
def get_products(regions):
return odoo.Odoo().get_products(regions)
erp_driver = erp_utils.load_erp_driver(CONF)
products = erp_driver.get_products(regions)
return products

View File

@ -7,13 +7,13 @@ port = 9999
[odoo]
version = 8.0
hostname =
hostname = localhost
port = 443
protocol = jsonrpc+ssl
database =
user =
password =
region_mapping =
database = prod
user = tester
password = passw0rd
region_mapping = nz_1:nz-1,nz_2:nz-2
[collector]
source = ceilometer

View File

@ -37,6 +37,7 @@ class DistilTestCase(base.BaseTestCase):
self.conf = cfg.ConfigOpts()
self.conf.register_opts(config.DEFAULT_OPTIONS)
self.conf.register_opts(config.ODOO_OPTS, group=config.ODOO_GROUP)
def setup_context(self, username="test_user", tenant_id="tenant_1",
auth_token="test_auth_token", tenant_name='test_tenant',

View File

@ -25,7 +25,7 @@ from distil.tests.unit import base
class TestCache(base.DistilTestCase):
config_file = 'distil_cache.conf'
config_file = 'distil.conf'
def setUp(self):
super(TestCache, self).setUp()

View File

View File

@ -0,0 +1,112 @@
# Copyright (c) 2017 Catalyst IT Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import mock
from collections import namedtuple
from distil.erp.drivers import odoo
from distil.tests.unit import base
REGION = namedtuple('Region', ['id'])
PRODUCTS = {'11': {'name_template': 'nz-1.c1.c1r1',
'lst_price': 0.00015,
'default_code': 'hour',
'description': '1 CPU, 1GB RAM'},
'22': {'name_template': 'nz-1.n1.router',
'lst_price': 0.00025,
'default_code': 'hour',
'description': 'Router'},
'33': {'name_template': 'nz-1.b1.volume',
'lst_price': 0.00035,
'default_code': 'hour',
'description': 'Block storage'},
'44': {'name_template': 'nz-1.o1.object',
'lst_price': 0.00045,
'default_code': 'hour',
'description': 'Object storage'}}
class TestOdooDriver(base.DistilTestCase):
config_file = 'distil.conf'
def setUp(self):
super(TestOdooDriver, self).setUp()
@mock.patch('odoorpc.ODOO')
@mock.patch('distil.common.openstack.get_regions')
def test_get_products(self, mock_get_regions, mock_odoo):
mock_get_regions.return_value = [REGION(id='nz-1'),
REGION(id='nz-2')]
odoodriver = odoo.OdooDriver(self.conf)
def _category_search(filters):
for filter in filters:
if filter[0] == 'name' and filter[2] == 'Compute':
return ['1']
if filter[0] == 'name' and filter[2] == 'Network':
return ['2']
if filter[0] == 'name' and filter[2] == 'Block Storage':
return ['3']
if filter[0] == 'name' and filter[2] == 'Object Storage':
return ['4']
def _product_search(filters):
for filter in filters:
if filter[0] == 'categ_id' and filter[2] == '1':
return ['11']
if filter[0] == 'categ_id' and filter[2] == '2':
return ['22']
if filter[0] == 'categ_id' and filter[2] == '3':
return ['33']
if filter[0] == 'categ_id' and filter[2] == '4':
return ['44']
def _odoo_execute(model, method, *args):
products = []
for id in args[0]:
products.append(PRODUCTS[id])
return products
odoodriver.odoo.execute = _odoo_execute
odoodriver.category = mock.Mock()
odoodriver.category.search = _category_search
odoodriver.product = mock.Mock()
odoodriver.product.search = _product_search
products = odoodriver.get_products(regions=['nz_1'])
self.assertEqual({'nz-1': {'block storage': [{'description':
'Block storage',
'price': 0.00035,
'resource': 'b1.volume',
'unit': 'hour'}],
'compute': [{'description':
'1 CPU, 1GB RAM',
'price': 0.00015,
'resource': 'c1.c1r1',
'unit': 'hour'}],
'network': [{'description': 'Router',
'price': 0.00025,
'resource': 'n1.router',
'unit': 'hour'}],
'object storage': [{'description':
'Object storage',
'price': 0.00045,
'resource': 'o1.object',
'unit': 'hour'}]}},
products)

View File

@ -49,6 +49,9 @@ distil.transformer =
fromimage = distil.transformer.conversion:FromImageTransformer
networkservice = distil.transformer.conversion:NetworkServiceTransformer
distil.erp =
odoo = distil.erp.drivers.odoo:OdooDriver
[build_sphinx]
all_files = 1
build-dir = doc/build