# -*- coding: utf8 -*-
################################################################################
#
# Copyright (c) 2012 STEPHANE MANGIN. (http://le-spleen.net) All Rights Reserved
# Stéphane MANGIN <stephane.mangin@freesbee.fr>
# Copyright (c) 2012 OSIELL SARL. (http://osiell.com) All Rights Reserved
# Eric Flaux <contact@osiell.com>
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsability of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# garantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program 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.
#
# This program 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 this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
################################################################################
"""
csv2oerp Access Programming Interface v0.6.
"""
import csv2oerp
from copy import deepcopy
from inspect import stack, getargvalues
from constants_and_vars import STATS
__all__ = ['connect', 'Import_session', 'BaseField', 'Custom', 'Relation', 'Custom']
ACTION_PATTERN = {
'skip': 'SKIP',
'replace': 'REPLACE',
'ignore': 'IGNORE',
'noupdate': 'NO_UPDATE',
'nocreate': 'NO_CREATE',
'unique': 'UNIQUE',
'required': 'REQUIRED',
'router': 'ROUTER',
}
SERVER = None
[docs]def connect(*args, **kwargs):
"""Set globals constants needed to initialize a connection to openerp
.. versionadded:: 0.5.3
:param host: IP or DNS name of the OERP server
:type host: str
:param port: Port number to reach
:type port: int
:param user: Username in the OERP server
:type user: str
:param pwd: Password of the username
:type pwd: str
:param dbname: Name of the database to reach
:type dbname: str
:raises: nothing
"""
global SERVER
SERVER = csv2oerp.api07.Openerp(*args, **kwargs)
[docs]class Import_session(object):
"""Main class which provides the functionnal part of the importation process.
.. note:: `sys.argv` integrated provides a command line parser.
Here are the available command line arguments::
-h, --help Show this kind of help message and exit
-o OFFSET, --offset=OFFSET Offset (Usually for header omission)
-l LIMIT, --limit=LIMIT Limit
-c, --check-mapping Check mapping template
-v, --verbose Verbose mode
-d, --debug
debug mode
-q, --quiet Doesn't print anything to stdout
"""
__slots__ = [
'_syslog_mode', '_columns_mapping', '_processed_lines', '_uid',
'_preconfigure', '_relationnal_prefix', '_current_mapping', '_encoding',
'_logger', '_opts', '_hdlr', '_lang',
'id', 'name', 'filename', 'delimiter', 'quotechar', 'encoding', 'offset',
'limit', 'models', 'kw', 'quiet', 'debug', '_session'
]
def __init__(self, **kw):
self.offset = 'offset' in kw and kw['offset']
self.limit = 'limit' in kw and kw['limit']
self.quiet = 'quiet' in kw and kw['quiet']
self.debug = 'debug' in kw and kw['debug']
self.kw = kw
self._relationnal_prefix = 'REL_'
self._columns_mapping = {}
self._current_mapping = None
self._session = None
#===========================================================================
# Accessors
#===========================================================================
@property
[docs] def server(self):
""" Return the current socket connection to the OpenERP server (xmlrpc).
"""
return SERVER
@property
[docs] def host(self):
""" Return the current connection host for this session.
"""
return SERVER.host
@property
[docs] def port(self):
""" Return the current connection port for this session.
"""
return SERVER.port
@property
[docs] def db(self):
""" Return the current database name for this session.
"""
return SERVER.db
@property
[docs] def user(self):
""" Return the current username for this session.
"""
return SERVER.user
@property
[docs] def uid(self):
""" Return the current UID for this session.
"""
return SERVER.uid
@property
[docs] def pwd(self):
""" Return the current password for this session.
"""
return SERVER.pwd
@property
[docs] def lines(self):
"""Getting all lines from CSV parser.
:returns: list
"""
self._session.lines()
@property
[docs] def mapping(self):
"""Getting columns mapping configuration.
"""
return self._columns_mapping
@property
[docs] def lang(self):
"""Getting current language.
"""
return SERVER.lang
[docs] def set_mapping(self, mapping):
""" Columns mapping configuration.
See ``Creation of your Columns mapping`` for further details.
"""
self._current_mapping = mapping
def set_lang(self, code='fr_FR'):
return super(Import_session, self).set_lang(code)
def set_logger(self, syslog=False):
return super(Import_session, self).set_logger(syslog)
[docs] def get_line_from_index(self, line_num):
""" Retrieve lines which have value at column
:param line_num: The index of line
:type line_num: value
:returns: tuple
"""
return self._session.get_line_from_index(line_num)
[docs] def get_lines_from_value(self, column, value):
""" Retrieve lines which have value at column
:param column: The column index
:type column: int
:param value: The value to compare from
:type value: value
:returns: tuple
"""
return self._session.get_lines_from_value(column, value)
[docs] def get_index_from_value(self, column, value, withcast=True):
""" Retrieve lines which have ``value`` at ``column``
By default ``value`` will be casted by ``column`` value type
overwrite it by withcast=False
:param column: The column index
:type column: int
:param value: The value to compare from
:type value: value
:returns: int
"""
return self._session.get_index_from_value(column, value,
withcast)
#===========================================================================
# Public methods
#===========================================================================
def log(self, level, msg, line=None, model=None):
return self._session.log(level, msg, line, model)
[docs] def connect(self, *args, **kwargs):
"""Set constants needed to initialize a connection to OpenERP.
:param host: IP or DNS name of the OERP server
:type host: str
:param port: Port number to reach
:type port: int
:param user: Username in the OERP server
:type user: str
:param pwd: Password of the username
:type pwd: str
:param dbname: Name of the database to reach
:type dbname: str
"""
global SERVER
SERVER = csv2oerp.api07.Openerp(*args, **kwargs)
#===========================================================================
# MiddleWare api06 to api07
#===========================================================================
[docs] def set_syslog(self, arg=True):
""" Set if syslog must be used instead of a log file.
"""
return self._session.set_syslog(arg)
[docs] def check_mapping(self):
"""Check mapping for semantics errors.
http://www.openerp.com/forum/topic31343.html
Also check for required and readonly fields.
"""
mapping = deepcopy(self._current_mapping)
title = "Errors occured during mapping check :\n___________________\n\n"
res = ""
# Mapping must be iterable throught pair of item
if not hasattr(mapping, 'iteritems'):
res += "\t* Mapping must be iterable throught pairs of item\n\n"
else:
for model, columns in mapping.iteritems():
# Check if model exist
if model.startswith(self._relationnal_prefix):
model = model.split('::')[1]
columns = [columns]
self._session._logger.info(
"Checking relationnal model '%s' ..." % model,
extra=STATS[self._session.id])
elif model.startswith('NO_CREATE'):
model = model.split('::')[1]
self._session._logger.info(
"Checking model (without creation) '%s' ..." % model,
extra=STATS[self._session.id])
elif model.startswith('NO_UPDATE'):
model = model.split('::')[1]
self._session._logger.info(
"Checking model (without update) '%s' ..." % model,
extra=STATS[self._session.id])
else:
self._session._logger.info(
"Checking model '%s' ..." % model,
extra=STATS[self._session.id])
# Check if value is a well formed tuple
for obj in columns:
if not hasattr(obj, 'iteritems'):
res += "\t* List of fields must be iterable \
throught pairs of item and objects (non relationnal) must be in a list.\n\n"
continue
for attr, tuple_val in deepcopy(obj).iteritems():
self._session._logger.debug(
"Checking field '%s'" % attr,
extra=STATS[self._session.id])
for patt in ACTION_PATTERN.values():
if attr.count(patt):
attr = attr.split('::')[1]
try:
if callable(tuple_val):
tuple_val = tuple_val()[0]
(column, lambda_func, searchable) = tuple_val
if column is None and not callable(lambda_func):
res += "\t* You must indicate either a \
column number or a function or both in a field definition, \n\n"
required = False
obj[attr] = (
column,
lambda_func,
searchable,
required)
except Exception as err:
print err
res += "\t* '%s' '%s' definition is not well \
formed, \n\n" % (model, attr)
if res != "":
raise Exception(title + res)
else:
return True
#===========================================================================
# Creation methods
#===========================================================================
[docs] def create(self, model, data, search=None):
"""Object's automatic and abstracted creation from model
Logged public method
:param model: Name of the OERP class model
:type model: str
:param data: Data dictionnary to transmit to OERP create/write method
:type data: dict
:param search: List of fields to be used for search
:type search: list
:returns: int
"""
if search is None:
search = []
if not data:
return []
(state, id_) = self._session._create(model, data, search)
return id_
def _fields_conversion(self, model, data):
"""Start fields conversion
"""
search = []
fields = []
for field, value in data.iteritems():
column = -1
searchable = False
required = False
try:
(column, callback, searchable) = value()[0]
except:
(column, callback, searchable, required) = value()[0]
# Add this item to search criteria
if searchable:
search.append(field)
fields.append(
csv2oerp.api07.Field(
field,
column,
callback,
required=required)
)
return fields, search
def _models_conversion(self):
"""Start models conversion.
"""
global SERVER
models_tmp = []
for model, datas in self._current_mapping.items():
update = not model.count('NO_UPDATE')
create = not model.count('NO_CREATE')
try:
model = model.split('::')[1]
except:
pass
# Iterate through multiple model's instances
for data in datas:
fields, search = self._fields_conversion(model, data)
# Create model and inject into models list
model_tmp = csv2oerp.api07.Model(
model,
fields=fields,
create=create,
update=update,
search=search)
models_tmp.append(model_tmp)
return models_tmp
def start(self):
if self.check_mapping():
models = self._models_conversion()
self._session.bind(SERVER, models)
return self._session.start()
return False
class BaseField(object):
def __init__(self, column, callback, search, attributes):
if not column or isinstance(column, int):
self.column = column
elif isinstance(column, list):
if column:
for index in column:
if not isinstance(index, int):
raise Exception('Column\'s `column` argument must be a list of int or int')
self.column = column
else:
raise Exception('Column\'s `column` argument must be a list of int or int')
else:
raise Exception(
'Column\'s `column` argument must be a list of int or int')
if callback and not callable(callback):
raise Exception('Column\'s `callback` argument must be callable')
self.callback = callback
if not isinstance(search, bool):
raise Exception('Column\'s `search` argument must be a boolean')
self.search = search
if attributes and not isinstance(attributes, list):
raise Exception('Column\'s `search` argument must be a list')
self.attributes = attributes
def __call__(self):
return ((self.column, self.callback, self.search), self.attributes)
class Column(BaseField):
""" Specify the column number and special treatments from which the current
model's field will be allocated to the object's creation.
Also declares metadatas for each model's field defined in mapping, like
``required``, ``readonly`` attributes.
.. versionadded: 0.6
Mapping example::
>>> {
... 'model': {
... 'field': Column(column=0, callback=None, search=True),
... }
... }
:param column: The actual column number (mandatory)
:type column: int
:param callback: The actual callback function
:type callback: function
:param search: Search for same value before create object
:type search: bool
:param required: Is this field is required
:type required: bool
:param skip: Is this field is skippable
:type skip: bool
:param ignore: Is this field is ignorable (So object creation is skipped)
:type ignore: bool
:param replace: Is this field is replaceable (So it can be redifined)
:type replace: bool
:param noupdate: Is this field have not to be updated if existing in database
:type noupdate: bool
:param unique: Is this model's instance must be unique inside current model
:type unique: bool
"""
def __init__(self, column=None, callback=None, search=False,
required=False, skip=False, ignore=False, replace=False,
noupdate=False, unique=False):
attributes = []
# Getting the calling frame from stack
frame = stack()[0][0]
# Arguments passed to the method
frame_args = getargvalues(frame)
# Reformatting to kwargs arguments
kwargs = frame_args.locals
# Cleaning kwargs from args
for item in ('column', 'callback', 'search', 'frame', 'action'):
if item in kwargs:
del kwargs[item]
for arg in kwargs:
if kwargs[arg] and arg in ACTION_PATTERN:
attributes.append(ACTION_PATTERN[arg])
return super(Column, self).__init__(column, callback, search, attributes)
[docs]class Relation(BaseField):
""" Specify a relation field.
Also declares metadatas like ``required``, ``readonly`` attributes.
.. versionadded: 0.6
Mapping example::
>>> {
... 'model': [
... {
... 'field': Relation('REL_custom::model', search=True),
... },
... ],
... 'REL_custom::model': {
... 'field': Column(1),
... }
... }
:param relation: The full name of the model which has to be related to field
:type relation: str
:param search: Search for same value before create object
:type search: bool
:param required: Is this field is required
:type required: bool
:param skip: Is this field is skippable
:type skip: bool
:param ignore: Is this field is ignorable (So object creation is skipped)
:type ignore: bool
:param replace: Is this field is replaceable (So it can be redifined)
:type replace: bool
:param noupdate: Is this field have not to be updated if existing in database
:type noupdate: bool
:param unique: Is this model's instance must be unique inside current model
:type unique: bool
"""
def __init__(self, relation, search=False, required=False, skip=False,
noupdate=False, unique=False):
callback = lambda *a: relation
attributes = []
# Getting the calling frame from stack
frame = stack()[0][0]
# Arguments passed to the method
frame_args = getargvalues(frame)
# Reformatting to kwargs arguments
kwargs = frame_args.locals
# Cleaning kwargs from args
for item in ('column', 'callback', 'search', 'frame', 'action'):
if item in kwargs:
del kwargs[item]
for arg in kwargs:
if kwargs[arg] and arg in ACTION_PATTERN:
attributes.append(ACTION_PATTERN[arg])
return super(Relation, self).__init__(None, callback, search, attributes)
[docs]class Custom(BaseField):
""" Specify a custom value for current field.
.. versionadded: 0.6
Mapping example::
>>> mapping = {
... 'model': {
... 'field': Custom('custom', search=True),
... }
... }
:param value: The value to apply.
:type value: type
:param search: Search for same value before create object
:type search: bool
"""
def __init__(self, value, search=False):
attributes = []
callback = lambda *a: value
# Getting the calling frame from stack
frame = stack()[0][0]
# Arguments passed to the method
frame_args = getargvalues(frame)
# Reformatting to kwargs arguments
kwargs = frame_args.locals
# Cleaning kwargs from args
for item in ('column', 'callback', 'search', 'frame', 'action'):
if item in kwargs:
del kwargs[item]
for arg in kwargs:
if kwargs[arg] and arg in ACTION_PATTERN:
attributes.append(ACTION_PATTERN[arg])
return super(Custom, self).__init__(None, callback, search, attributes)
class Router(BaseField):
""" Specify the column number and special treatments from which the current
model's field will be allocated to the object's creation.
Also declares metadatas for each model's field defined in mapping, like
``required``, ``readonly`` attributes.
.. versionadded: 0.6
Mapping example::
>>> def _router(iself, model, field, value, line):
... if value in 'some_value':
... return {'field': {'f1':'val', 'f2':'val', 'f3':'val'}}
>>> ...
>>> {
... 'model': {
... 'field': Router(column=0, callback=_router),
... }
... }
:param column: The actual column number (mandatory)
:type column: int
:param callback: The actual callback function
:type callback: function
"""
def __init__(self, column=None, callback=None, **kwargs):
attributes = []
# Getting the calling frame from stack
frame = stack()[0][0]
# Arguments passed to the method
frame_args = getargvalues(frame)
# Reformatting to kwargs arguments
kwargs = frame_args.locals
# Cleaning kwargs from args
for item in ('column', 'callback', 'frame', 'action'):
if item in kwargs:
del kwargs[item]
for arg in kwargs:
if kwargs[arg] and arg in ACTION_PATTERN:
attributes.append(ACTION_PATTERN[arg])
return super(Router, self).__init__(column, callback, False, attributes)