Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ADD] connector_redmine module #23

Open
wants to merge 2 commits into
base: 12.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions connector_redmine/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
.. image:: https://img.shields.io/badge/license-AGPL--3-blue.png
:target: https://www.gnu.org/licenses/agpl
:alt: License: AGPL-3

=================
Redmine Connector
=================

Base connector module for Redmine.

It allows the authentication to a Redmine instance using the REST API.

It also defines a method getUser that searches for the Redmine user related
to the Odoo user.

Be aware that the user login must be the same in both systems.

Installation
============

# Install Redmine
Refer to http://www.redmine.org/projects/redmine/wiki/redmineinstall

# Install python-redmine
sudo pip install python-redmine


Configuration
=============

## Add this to the end of the openerp-server.conf.

server_wide_modules = web, queue_job
is_job_channel_available = True

[queue_job]
channels = root: 3

## Create a backend

- Go to Connectors -> Redmine -> Backends
- Odoo user must be “Job Queue Manager” and “Connector Manager” to create new backends.
- New backends can be created under Connector / Redmine / Backends.
- Enter the URL to the Redmine
- Enter the admin's API key
- Enter the word "contract_ref" (without quotation marks) in "Contract # field name"
- For the Redmine projects, the name of the Odoo project must be entered in the field
- Click on the button to test the connection

## Odoo projects

- For the Redmine projects to be synchronized, projects must be created in Odoo.
- The name of the Odoo project must be entered in Redmine under configuration in the “contract_ref” field

## Odoo project stages

- For the ticket status in Redmine you need Odoo levels.
- At the levels there is a field for the Redmine status, which should be mapped to this level.
- The levels must apply to the project.

## Odoo users

- Create Odoo users who have the same login name as in Redmine or their email address.
- Create a default user for each project, which will always be assigned to tasks / time records if it is not an Odoo user.
- This must be entered in the project.

## Odoo employees

- Create Odoo employees and link them to the Odoo users.
- If not, the time entries are created without employees

## Notes

- The project has the "last imported on" field.
- This does not correspond to the time at which the function was last called in Odoo, but:
- During the last import, tickets that were last updated in Redmine on a specific date were synchronized.
- The "latest" date of all these tickets is now "last imported on".
- Attention : If a time entry is added or changed in Redmine, this does NOT change the “last updated” date of the Redmine ticket!
4 changes: 4 additions & 0 deletions connector_redmine/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

from . import models
from . import components
34 changes: 34 additions & 0 deletions connector_redmine/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

{
"name": "Redmine Connector",
"version": "12.0.1.0.0",
"author": "Odoo Community Association (OCA)/Elego Software Solutions GmbH",
"category": "Connector",
"website": "https://github.com/OCA/connector-redmine",
"license": "AGPL-3",
"depends": [
"connector",
"account",
"hr_timesheet",
"project",
"project_task_default_stage",
"project_task_add_very_high",
],
"external_dependencies": {
"python": [
"textile",
"redminelib",
],
},
"data": [
"security/ir.model.access.csv",
"data/cron_import_issue.xml",
"views/redmine_backend_view.xml",
"views/redmine_menu.xml",
"views/hr_timesheet_view.xml",
"views/project_views.xml",
],
"application": True,
"installable": True,
}
6 changes: 6 additions & 0 deletions connector_redmine/components/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

from . import base
from . import backend
from . import issue
from . import time_entry
3 changes: 3 additions & 0 deletions connector_redmine/components/backend/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

from . import adapter
10 changes: 10 additions & 0 deletions connector_redmine/components/backend/adapter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

from odoo.addons.component.core import Component


class BackendAdapter(Component):

_name = "redmine.backend.adapter"
_inherit = "redmine.adapter"
_apply_on = "redmine.backend"
6 changes: 6 additions & 0 deletions connector_redmine/components/base/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

from . import adapter
from . import binder
from . import synchronizer
from . import mapper
79 changes: 79 additions & 0 deletions connector_redmine/components/base/adapter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

import logging

import odoo.addons.component.exception as cn_exception # port to v12
from odoo.addons.component.core import AbstractComponent
from odoo.tools import ustr
from odoo.tools.translate import _
from requests import exceptions

_logger = logging.getLogger(__name__)

try:
from redminelib import Redmine, exceptions
except (ImportError, IOError) as err:
_logger.warning("python-redmine not installed!")


class RedmineAdapter(AbstractComponent):
"""
Backend Adapter for Redmine

Read methods must return a python dictionary and search methods a list
of ids.

If a Redmine record is not found in a read method, the return value
must be None.

This is important because it allows to mock the adapter easily
in unit tests.
"""

_name = "redmine.adapter"
_inherit = ["base.backend.adapter", "base.connector"]
_usage = "backend.adapter"

def _auth(self):
backend = self.backend_record
auth_data = backend.sudo().read(["location", "key"])[0]

requests = {"verify": backend.verify_ssl}
if backend.proxy:
requests["proxies"] = dict([backend.proxy.split("://")])
cnx_options = {"requests": requests}

try:
redmine_api = Redmine(
auth_data["location"], key=auth_data["key"], **cnx_options
)
redmine_api.auth()

except (exceptions.AuthError, exceptions.ConnectionError) as e:
raise cn_exception.FailedJobError(
_("Redmine connection Error: " "Invalid authentications key. (%s)") % e
)

except (exceptions.UnknownError, exceptions.ServerError) as e:
raise cn_exception.NetworkRetryableError(
_("A network error caused the failure of the job: " "%s") % ustr(e)
)

self.redmine_api = redmine_api

def search_user(self, login):
"""
Get a Redmine user id from a Odoo login
"""
self._auth()

users = self.redmine_api.user.filter(name=login)

user_id = next((user.id for user in users if user.login == login), False)

if not user_id:
raise cn_exception.InvalidDataError(
_("No user with login %s found in Redmine.") % login
)

return user_id
12 changes: 12 additions & 0 deletions connector_redmine/components/base/binder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

from odoo.addons.component.core import AbstractComponent


class RedmineModelBinder(AbstractComponent):
_name = "redmine.binder"
_inherit = "base.binder"
_external_field = "redmine_id"
_backend_field = "backend_id"
_odoo_field = "odoo_id"
_sync_date_field = "sync_date"
50 changes: 50 additions & 0 deletions connector_redmine/components/base/mapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

from datetime import datetime

from odoo import fields
from odoo.addons.component.core import AbstractComponent, Component
from odoo.addons.connector.components.mapper import mapping


class RedmineImportMapper(AbstractComponent):
_name = "redmine.import.mapper"
_inherit = "base.import.mapper"
_usage = "import.mapper"

@mapping
def backend_id(self, record):
return {"backend_id": self.backend_record.id}

@mapping
def updated_on(self, record):
date = record["updated_on"]
return {"updated_on": fields.Datetime.to_string(date)}

@mapping
def sync_date(self, record):
date = datetime.now()
return {"sync_date": fields.Datetime.to_string(date)}


class RedmineImportMapChild(Component):
_name = "redmine.import.child.mapper"
_inherit = "base.map.child.import"
_usage = "import.map.child"

def get_item_values(self, map_record, to_attr, options):
""" Resolve the ids for updates """
values = map_record.values(**options)
binding = self.binder_for().to_internal(values["redmine_id"])
if binding:
values["id"] = binding.id
return values

def format_items(self, items_values):
"""Updates children when they already exist in the DB"""

def format_item(values):
_id = values.pop("id", None)
return (1, _id, values) if _id else (0, 0, values)

return [format_item(values) for values in items_values]
92 changes: 92 additions & 0 deletions connector_redmine/components/base/synchronizer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

import logging

from odoo.addons.component.core import AbstractComponent
from odoo.addons.connector.exception import IDMissingInBackend
from odoo.tools.translate import _

_logger = logging.getLogger(__name__)


class RedmineImporter(AbstractComponent):
""" Base importer for Redmine """

_name = "redmine.importer"
_inherit = "base.importer"
_usage = "record.importer"

def __init__(self, work_context):
"""
:param environment: current environment (backend, ...)
:type environment: :py:class:`connector.connector.ConnectorEnvironment`
"""
super(RedmineImporter, self).__init__(work_context)
self.redmine_id = None
self.updated_on = None

def _get_redmine_data(self):
"""Return the raw Redmine data for ``self.redmine_id`` in a dict"""
return self.backend_adapter.read(self.redmine_id)

def _map_data(self):
"""
Return an instance of
:py:class:`~odoo.addons.connector.unit.mapper.MapRecord`
"""
return self.mapper.map_record(self.redmine_record)

def _get_binding(self):
"""Return the binding id from the redmine id"""
return self.binder.to_internal(self.redmine_id)

def _create_data(self, map_record, **kwargs):
return map_record.values(for_create=True, **kwargs)

def _create(self, data):
""" Create the Odoo record """
# special check on data before import
model = self.model.with_context(connector_no_export=True)
binding = model.create(data)
_logger.debug("%d created from redmine %s", binding, self.redmine_id)
return binding

def _update_data(self, map_record, **kwargs):
return map_record.values(**kwargs)

def _update(self, binding, data):
""" Update an Odoo record """
# special check on data before import
binding.with_context(connector_no_export=True).write(data)
_logger.debug("%d updated from redmine %s", binding, self.redmine_id)
return

def run(self, redmine_id):
"""Run the synchronization

:param redmine_id: identifier of the record on Redmine
:param options: dict of parameters used by the synchronizer
"""
self.redmine_id = redmine_id
try:
self.redmine_record = self._get_redmine_data()
except IDMissingInBackend:
return _("Record does no longer exist in Redmine.")

# Case where the redmine record is not found in the backend.
if self.redmine_record is None:
return

binding = self._get_binding()

map_record = self._map_data()
self.updated_on = map_record.values()["updated_on"]

if binding:
record = self._update_data(map_record)
self._update(binding, record)
else:
record = self._create_data(map_record)
binding = self._create(record)

self.binder.bind(self.redmine_id, binding)
6 changes: 6 additions & 0 deletions connector_redmine/components/issue/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

from . import adapter
from . import binder
from . import synchronizer
from . import mapper
Loading