From 550444020c7c950a1629e39d9879436fee2ece9a Mon Sep 17 00:00:00 2001 From: cpatel Date: Tue, 6 Jul 2021 08:05:43 +0200 Subject: [PATCH 1/2] [ADD] connector_redmine module --- connector_redmine/README.rst | 78 ++ connector_redmine/__init__.py | 4 + connector_redmine/__manifest__.py | 33 + connector_redmine/components/__init__.py | 6 + .../components/backend/__init__.py | 3 + .../components/backend/adapter.py | 10 + connector_redmine/components/base/__init__.py | 6 + connector_redmine/components/base/adapter.py | 79 ++ connector_redmine/components/base/binder.py | 12 + connector_redmine/components/base/mapper.py | 50 + .../components/base/synchronizer.py | 92 ++ .../components/issue/__init__.py | 6 + connector_redmine/components/issue/adapter.py | 157 +++ connector_redmine/components/issue/binder.py | 9 + connector_redmine/components/issue/mapper.py | 234 ++++ .../components/issue/synchronizer.py | 72 ++ .../components/time_entry/__init__.py | 6 + .../components/time_entry/adapter.py | 89 ++ .../components/time_entry/binder.py | 9 + .../components/time_entry/mapper.py | 133 ++ .../components/time_entry/synchronizer.py | 106 ++ connector_redmine/data/cron_import_issue.xml | 18 + connector_redmine/i18n/connector_redmine.pot | 1075 ++++++++++++++++ connector_redmine/i18n/de.po | 1087 +++++++++++++++++ connector_redmine/models/__init__.py | 8 + connector_redmine/models/analytic_account.py | 15 + connector_redmine/models/project.py | 159 +++ connector_redmine/models/redmine_backend.py | 110 ++ connector_redmine/models/redmine_binding.py | 16 + connector_redmine/models/redmine_issue.py | 66 + .../security/ir.model.access.csv | 7 + connector_redmine/static/src/img/icon.png | Bin 0 -> 10600 bytes connector_redmine/static/src/img/icon.svg | 35 + connector_redmine/tests/__init__.py | 4 + connector_redmine/tests/common.py | 136 +++ .../tests/test_connector_redmine.py | 118 ++ connector_redmine/tests/test_import_issues.py | 177 +++ connector_redmine/views/hr_timesheet_view.xml | 15 + connector_redmine/views/project_views.xml | 87 ++ .../views/redmine_backend_view.xml | 76 ++ connector_redmine/views/redmine_menu.xml | 15 + 41 files changed, 4418 insertions(+) create mode 100644 connector_redmine/README.rst create mode 100644 connector_redmine/__init__.py create mode 100644 connector_redmine/__manifest__.py create mode 100644 connector_redmine/components/__init__.py create mode 100644 connector_redmine/components/backend/__init__.py create mode 100644 connector_redmine/components/backend/adapter.py create mode 100644 connector_redmine/components/base/__init__.py create mode 100644 connector_redmine/components/base/adapter.py create mode 100644 connector_redmine/components/base/binder.py create mode 100644 connector_redmine/components/base/mapper.py create mode 100644 connector_redmine/components/base/synchronizer.py create mode 100644 connector_redmine/components/issue/__init__.py create mode 100644 connector_redmine/components/issue/adapter.py create mode 100644 connector_redmine/components/issue/binder.py create mode 100644 connector_redmine/components/issue/mapper.py create mode 100644 connector_redmine/components/issue/synchronizer.py create mode 100644 connector_redmine/components/time_entry/__init__.py create mode 100644 connector_redmine/components/time_entry/adapter.py create mode 100644 connector_redmine/components/time_entry/binder.py create mode 100644 connector_redmine/components/time_entry/mapper.py create mode 100644 connector_redmine/components/time_entry/synchronizer.py create mode 100644 connector_redmine/data/cron_import_issue.xml create mode 100644 connector_redmine/i18n/connector_redmine.pot create mode 100644 connector_redmine/i18n/de.po create mode 100644 connector_redmine/models/__init__.py create mode 100644 connector_redmine/models/analytic_account.py create mode 100644 connector_redmine/models/project.py create mode 100644 connector_redmine/models/redmine_backend.py create mode 100644 connector_redmine/models/redmine_binding.py create mode 100644 connector_redmine/models/redmine_issue.py create mode 100644 connector_redmine/security/ir.model.access.csv create mode 100644 connector_redmine/static/src/img/icon.png create mode 100644 connector_redmine/static/src/img/icon.svg create mode 100644 connector_redmine/tests/__init__.py create mode 100644 connector_redmine/tests/common.py create mode 100644 connector_redmine/tests/test_connector_redmine.py create mode 100644 connector_redmine/tests/test_import_issues.py create mode 100644 connector_redmine/views/hr_timesheet_view.xml create mode 100644 connector_redmine/views/project_views.xml create mode 100644 connector_redmine/views/redmine_backend_view.xml create mode 100644 connector_redmine/views/redmine_menu.xml diff --git a/connector_redmine/README.rst b/connector_redmine/README.rst new file mode 100644 index 0000000..e7ee8c1 --- /dev/null +++ b/connector_redmine/README.rst @@ -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! diff --git a/connector_redmine/__init__.py b/connector_redmine/__init__.py new file mode 100644 index 0000000..ee16821 --- /dev/null +++ b/connector_redmine/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import models +from . import components diff --git a/connector_redmine/__manifest__.py b/connector_redmine/__manifest__.py new file mode 100644 index 0000000..95e4d62 --- /dev/null +++ b/connector_redmine/__manifest__.py @@ -0,0 +1,33 @@ +# 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", + ], + }, + "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, +} diff --git a/connector_redmine/components/__init__.py b/connector_redmine/components/__init__.py new file mode 100644 index 0000000..e88490b --- /dev/null +++ b/connector_redmine/components/__init__.py @@ -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 diff --git a/connector_redmine/components/backend/__init__.py b/connector_redmine/components/backend/__init__.py new file mode 100644 index 0000000..6ee13d1 --- /dev/null +++ b/connector_redmine/components/backend/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import adapter diff --git a/connector_redmine/components/backend/adapter.py b/connector_redmine/components/backend/adapter.py new file mode 100644 index 0000000..33ebf12 --- /dev/null +++ b/connector_redmine/components/backend/adapter.py @@ -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" diff --git a/connector_redmine/components/base/__init__.py b/connector_redmine/components/base/__init__.py new file mode 100644 index 0000000..7eec9e9 --- /dev/null +++ b/connector_redmine/components/base/__init__.py @@ -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 diff --git a/connector_redmine/components/base/adapter.py b/connector_redmine/components/base/adapter.py new file mode 100644 index 0000000..f2ddd4b --- /dev/null +++ b/connector_redmine/components/base/adapter.py @@ -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.exceptions import ConnectionError + +_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, 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 diff --git a/connector_redmine/components/base/binder.py b/connector_redmine/components/base/binder.py new file mode 100644 index 0000000..89552ec --- /dev/null +++ b/connector_redmine/components/base/binder.py @@ -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" diff --git a/connector_redmine/components/base/mapper.py b/connector_redmine/components/base/mapper.py new file mode 100644 index 0000000..58ca986 --- /dev/null +++ b/connector_redmine/components/base/mapper.py @@ -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] diff --git a/connector_redmine/components/base/synchronizer.py b/connector_redmine/components/base/synchronizer.py new file mode 100644 index 0000000..3eaa18d --- /dev/null +++ b/connector_redmine/components/base/synchronizer.py @@ -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) diff --git a/connector_redmine/components/issue/__init__.py b/connector_redmine/components/issue/__init__.py new file mode 100644 index 0000000..7eec9e9 --- /dev/null +++ b/connector_redmine/components/issue/__init__.py @@ -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 diff --git a/connector_redmine/components/issue/adapter.py b/connector_redmine/components/issue/adapter.py new file mode 100644 index 0000000..2761921 --- /dev/null +++ b/connector_redmine/components/issue/adapter.py @@ -0,0 +1,157 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import logging + +import odoo.addons.connector.exception as cn_exception +from odoo.addons.component.core import Component +from odoo.tools.translate import _ +from redminelib import exceptions + +_logger = logging.getLogger(__name__) + + +class RedmineIssueAdapter(Component): + _name = "redmine.issue.adapter" + _inherit = "redmine.adapter" + _apply_on = "redmine.issue" + _collection = "redmine.backend" + + def search(self, project_name, project_url, last_imported_on=None): + """ + Get all issues filtered by project or those currently updated or + whose time entries were currently updated. + :param project_name: string containing the project's name in Odoo + :param project_url: string containing the project's url + :param last_imported_on: datetime with time of last imported record + :return: + """ + self._auth() + + redmine_project_id = self._get_redmine_project(project_name, project_url) + + if last_imported_on: + issues = [] + for issue in self.redmine_api.issue.all(): + if issue.project.id == redmine_project_id: + if last_imported_on < issue.updated_on: + issues.append(issue.id) + else: + for time_entry in issue.time_entries: + if last_imported_on < time_entry.updated_on: + issues.append(issue.id) + break + return issues + else: + return [ + issue.id + for issue in self.redmine_api.issue.all() + if issue.project.id == redmine_project_id + ] + + def _get_redmine_project(self, project_name, project_url): + """ + Get project id of redmine project by its Odoo name or its URL. + :param project_name: string containing the project's name in Odoo + :param project_url: string containing the project's url + :return: integer id of found redmine project + """ + redmine_project_ids = [] + for project in self.redmine_api.project.all(): + if hasattr(project, "custom_fields"): + for field in project.custom_fields: + if hasattr(field, "name") and hasattr(field, "value"): + if ( + field.name == self.backend_record.contract_ref + and field.value == project_name + ): + redmine_project_ids.append(project.id) + else: + if not project_url: + raise Exception( + _( + "There is no Redmine project URL given to identify correct redmine project." + ) + ) + # URLs in Redmine api are stored without appended '/' + if project_url[-1] == "/": + project_url = project_url[:-1] + + if project.url == project_url: + redmine_project_ids.append(project.id) + + if not redmine_project_ids: + raise Exception( + _( + "There is no Redmine project with given name: '%s' or URL: '%s'." + % (project_name, project_url) + ) + ) + if len(redmine_project_ids) > 1: + raise Exception( + _( + "There is more than one Redmine project with given name: '%s' or URL: '%s'." + % (project_name, project_url) + ) + ) + + return redmine_project_ids[0] + + def read(self, record_id): + self._auth() + + try: + issue = self.redmine_api.issue.get(record_id) + except exceptions.ResourceNotFoundError: + return None + except exceptions.ForbiddenError: + return None + + project = self.redmine_api.project.get(issue.project.id) + custom_field = self.backend_record.contract_ref + + if hasattr(project, "custom_fields"): + contract_ref = next( + ( + field.value + for field in project.custom_fields + if field.name == custom_field + ), + False, + ) + elif hasattr(project, self.backend_record.contract_ref): + contract_ref = getattr(project, self.backend_record.contract_ref) + + if not contract_ref: + raise cn_exception.InvalidDataError( + _("The field %(field)s is not set in Redmine for project %(project)s.") + % { + "field": custom_field, + "project": project.name, + } + ) + + user = issue.assigned_to if hasattr(issue, "assigned_to") else False + + try: + user = self.redmine_api.user.get(user.id) if user else False + except exceptions.ResourceNotFoundError: + _logger.warning( + _("Redmine-Issue: %s - The user %s does not exist in Redmine anymore.") + % (issue.id, user.name if user else "") + ) + + user_login = user.login if hasattr(user, "login") else "" + + res = { + "project": project, + "contract_ref": contract_ref, + "issue": issue, + "issue_id": issue.id, + "updated_on": issue.updated_on, + "assigned_user": issue.assigned_to + if hasattr(issue, "assigned_to") + else False, + "assigned_user_login": user_login, + "time_entries": issue.time_entries, + } + return res diff --git a/connector_redmine/components/issue/binder.py b/connector_redmine/components/issue/binder.py new file mode 100644 index 0000000..0215882 --- /dev/null +++ b/connector_redmine/components/issue/binder.py @@ -0,0 +1,9 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo.addons.component.core import Component + + +class RedmineIssueBinder(Component): + _name = "redmine.issue.binder" + _inherit = "redmine.binder" + _apply_on = "redmine.issue" diff --git a/connector_redmine/components/issue/mapper.py b/connector_redmine/components/issue/mapper.py new file mode 100644 index 0000000..55413ff --- /dev/null +++ b/connector_redmine/components/issue/mapper.py @@ -0,0 +1,234 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import logging + +from odoo.addons.component.core import Component +from odoo.addons.connector.components.mapper import mapping +from odoo.addons.connector.exception import MappingError +from odoo.tools.translate import _ +from textile import textile + +_logger = logging.getLogger(__name__) + + +class RedmineIssueImportMapper(Component): + _name = "redmine.issue.mapper" + _inherit = "redmine.import.mapper" + _apply_on = "redmine.issue" + + direct = [ + ("issue_id", "redmine_id"), + ] + + children = [("time_entries", "timesheet_ids", "redmine.account.analytic.line")] + + def _map_child(self, map_record, from_attr, to_attr, model_name): + issue_id = map_record.source["issue_id"] + child_records = map_record.source[from_attr] + # Use the adapter read method to get info about user_login and contract_ref + adapter = self.component(usage="backend.adapter", model_name=model_name) + # In Redmine a parent issue can have several child-issues, + # which can have several time entries. The children's time + # entries are also connected with the parent issue. + # If the time entries do not belong to the current issue + # (but to a child), they should not be processed. + detail_records = [ + adapter.read(child_record.id) + for child_record in child_records + if child_record.issue.id == issue_id + ] + mapper_child = self._get_map_child_component(model_name) + items = mapper_child.get_items( + detail_records, map_record, to_attr, options=self.options + ) + return items + + @mapping + def child_ids(self, record): + issue = record["issue"] + children = issue.children if hasattr(issue, "children") else [] + issue_binder = self.binder_for("redmine.issue") + redmine_issue_children = [] + for child in children: + redmine_issue_child = issue_binder.to_internal(child.id) + if redmine_issue_child: + redmine_issue_children.append(redmine_issue_child.odoo_id.id) + return {"child_ids": [(6, 0, redmine_issue_children)]} + + @mapping + def date_deadline(self, record): + issue = record["issue"] + return { + "date_deadline": issue.due_date if hasattr(issue, "due_date") else False + } + + @mapping + def description(self, record): + html = textile(record["issue"].description) + html = html.replace("", '
') + html = html.replace("
", "
") + return {"description": html} + + @mapping + def last_update(self, record): + return {"last_update": record["updated_on"]} + + @mapping + def name(self, record): + res = {"name": "#" + str(record["issue_id"]) + " - " + record["issue"].subject} + return res + + @mapping + def parent_id(self, record): + issue = record["issue"] + parent = issue.parent if hasattr(issue, "parent") else False + redmine_issue = ( + self.binder_for("redmine.issue").to_internal(parent.id) if parent else False + ) + return {"parent_id": redmine_issue.odoo_id.id if redmine_issue else False} + + @mapping + def planned_hours(self, record): + issue = record["issue"] + return { + "planned_hours": issue.estimated_hours + if hasattr(issue, "estimated_hours") + else 0 + } + + @mapping + def priority(self, record): + issue = record["issue"] + priority = "0" + if str(issue.priority) == "Normal": + priority = "1" + elif str(issue.priority) == "High": + priority = "2" + elif str(issue.priority) == "Urgent": + priority = "4" + elif str(issue.priority) == "Immediately": + priority = "5" + return {"priority": priority if hasattr(issue, "priority") else 0} + + @mapping + def progress(self, record): + # if ('issue' not in record): + # import wdb; wdb.set_trace() + issue = record["issue"] + return { + "progress": record["issue"].done_ratio + if hasattr(issue, "done_ratio") + else 0 + } + + @mapping + def project_id(self, record): + projects = self.env["project.project"].search( + [ + ("name", "=", record["contract_ref"]), + ] + ) + if not projects: + raise MappingError( + _("No Odoo project with name '%s' exists.") % record["contract_ref"] + ) + if len(projects) > 1: + raise MappingError( + _("There are more Odoo projects with name '%s'.") + % record["contract_ref"] + ) + return {"project_id": projects[0].id} + + @mapping + def redmine_issue_url(self, record): + return {"redmine_issue_url": record["issue"].url} + + @mapping + def stage_id(self, record): + redmine_issue_status = record["issue"].status + project_id = self.project_id(record)["project_id"] + project_name = record["contract_ref"] + + task_type = self.env["project.task.type"].search( + [ + ("project_ids", "=", project_id), + ("redmine_issue_state", "=", redmine_issue_status), + ] + ) + if len(task_type) == 0: + raise MappingError( + _( + "There is no project task type for the given project '%s' " + "that matches the Redmine state '%s'.\n" + "Add the Redmine state to an existing project task type or " + "create a new one.\n" + "Make also sure that the task type is available for this project." + ) + % (project_name, redmine_issue_status) + ) + elif len(task_type) > 1: + raise MappingError( + _( + "There are more project task types for the given project '%s' " + "that match the Redmine state '%s'.\n" + "Check for example these types: '%s' and '%s'." + ) + % ( + project_name, + redmine_issue_status, + task_type[0].name, + task_type[1].name, + ) + ) + return {"stage_id": task_type[0].id} + + @mapping + def user_id(self, record): + user_obj = self.env["res.users"] + user = None + + login = record["assigned_user_login"] if "assigned_user_login" in record else "" + name = ( + record["assigned_user"].name + if "assigned_user" in record and record["assigned_user"] + else "" + ) + + if login: + user = user_obj.search([("login", "=", login)]) + + if not user: + user = user_obj.search([("name", "=", name)]) + + if not user: + project_id = self.project_id(record)["project_id"] + user = ( + self.env["project.project"].browse(project_id).default_redmine_user + ) + + if len(user) > 1: + raise MappingError( + _( + "There is more than one user in Odoo with login name: '%s' or name: '%s'." + ) + % (login, name) + ) + + return { + "user_id": user.id if user else None, + "redmine_user": name, + } + + def finalize(self, map_record, values): + """ + For Redmine issues without estimated hours the planned hours in Odoo + are computed by total spent time from time entries and issue's progress value. + If the latter is also not set in Redmine, it is interpreted as 5% progress. + """ + if not values["planned_hours"]: + spent_hours = map_record.source["issue"].spent_hours + if not values["progress"] and spent_hours: + values["progress"] = 5.0 + if values["progress"]: + values["planned_hours"] = 100 * spent_hours / values["progress"] + return values diff --git a/connector_redmine/components/issue/synchronizer.py b/connector_redmine/components/issue/synchronizer.py new file mode 100644 index 0000000..eb6fe07 --- /dev/null +++ b/connector_redmine/components/issue/synchronizer.py @@ -0,0 +1,72 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from datetime import datetime + +from odoo.addons.component.core import Component +from odoo.addons.connector.exception import MappingError +from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT + + +class RedmineIssueBatchImportSynchronizer(Component): + _name = "redmine.issue.batch.importer" + _inherit = "redmine.importer" + _usage = "batch.importer" + _apply_on = "redmine.issue" + + def _import_record(self, external_id): + """ Delay the import of the records""" + delayable = self.model.with_delay() + delayable.import_record(self.backend_record, external_id) + + def run_single_project(self, project_name, project_url, last_imported_on=None): + """ + Run Redmine Import without cron jobs. + :param project_name: string to identify redmine project by its custom field + :param project_url: string to identify redmine project by its url + :param filters: dictionary containing redmine fields to filter records + :param last_imported_on: datetime to get only currently updated records + :return: list of mapping errors + """ + record_ids = self.backend_adapter.search( + project_name=project_name, + project_url=project_url, + last_imported_on=last_imported_on, + ) + + # TODO: Collect all mapping errors and post them on project + mapping_errors = [] + + for record_id in record_ids: + try: + self._import_record(record_id) + except MappingError as err: + mapping_errors.append(err) + + return mapping_errors + + +class RedmineIssueImportSynchronizer(Component): + _name = "redmine.issue.importer" + _inherit = "redmine.importer" + _apply_on = "redmine.issue" + + def run(self, record_id): + """ + Update the last synchronization date on the backend record + + The schedule date of the import batch in Odoo can not be + used because the time zone can be different in both systems. + + Each time a batch is imported, we take the record with the + highest value for updated_on. + """ + super(RedmineIssueImportSynchronizer, self).run(record_id) + + project = self.env["project.project"].search( + [("name", "=", self.redmine_record["contract_ref"])] + ) + + # check also the time_entries for their last updated_on, could be later than the issue updated_on + updated_on = datetime.strptime(self.updated_on, DEFAULT_SERVER_DATETIME_FORMAT) + if not project.last_imported_on or updated_on > project.last_imported_on: + project.write({"last_imported_on": self.updated_on}) diff --git a/connector_redmine/components/time_entry/__init__.py b/connector_redmine/components/time_entry/__init__.py new file mode 100644 index 0000000..7eec9e9 --- /dev/null +++ b/connector_redmine/components/time_entry/__init__.py @@ -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 diff --git a/connector_redmine/components/time_entry/adapter.py b/connector_redmine/components/time_entry/adapter.py new file mode 100644 index 0000000..c84233a --- /dev/null +++ b/connector_redmine/components/time_entry/adapter.py @@ -0,0 +1,89 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import logging + +import odoo.addons.connector.exception as cn_exception +from odoo.addons.component.core import Component +from odoo.tools.translate import _ +from redminelib import exceptions + +_logger = logging.getLogger(__name__) + + +class RedmineTimeEntryAdapter(Component): + _name = "redmine.account.analytic.line.adapter" + _inherit = "redmine.adapter" + _apply_on = "redmine.account.analytic.line" + _collection = "redmine.backend" + + def read(self, record_id): + self._auth() + + try: + time_entry = self.redmine_api.time_entry.get(record_id) + except exceptions.ResourceNotFoundError: + return None + except exceptions.ForbiddenError: + return None + + project = self.redmine_api.project.get(time_entry.project.id) + custom_field = self.backend_record.contract_ref + + if hasattr(project, "custom_fields"): + contract_ref = next( + ( + field.value + for field in project.custom_fields + if field.name == custom_field + ), + False, + ) + elif hasattr(project, self.backend_record.contract_ref): + contract_ref = getattr(project, self.backend_record.contract_ref) + + if not contract_ref: + raise cn_exception.InvalidDataError( + _("The field %(field)s is not set in Redmine for project %(project)s.") + % {"field": custom_field, "project": project.name} + ) + + user = time_entry.user if hasattr(time_entry, "user") else False + + try: + user = self.redmine_api.user.get(user.id) if user else False + except exceptions.ResourceNotFoundError: + _logger.warning( + _("Redmine-Issue: %s - The user %s does not exist in Redmine anymore.") + % (time_entry.issue.id, user.name if user else "") + ) + + user_login = user.login if hasattr(user, "login") else "" + + res = { + "time_entry": time_entry, + "entry_id": time_entry.id, + "entry_hours": time_entry.hours, + "project": project, + "contract_ref": contract_ref, + "updated_on": time_entry.updated_on, + "user": time_entry.user if hasattr(time_entry, "user") else False, + "user_login": user_login, + } + return res + + def search(self, updated_from, filters): + """ + Get all time entry that were updated between the interval of time + """ + self._auth() + + if updated_from: + return [ + entry.id + for entry in self.redmine_api.time_entry.filter(**filters) + if updated_from < entry.updated_on + ] + else: + # If the time entries are imported for the first time + # no need to filter by the date of update + return [entry.id for entry in self.redmine_api.time_entry.filter(**filters)] diff --git a/connector_redmine/components/time_entry/binder.py b/connector_redmine/components/time_entry/binder.py new file mode 100644 index 0000000..5d083af --- /dev/null +++ b/connector_redmine/components/time_entry/binder.py @@ -0,0 +1,9 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo.addons.component.core import Component + + +class RedmineTimeEntryBinder(Component): + _name = "redmine.account.analytic.line.binder" + _inherit = "redmine.binder" + _apply_on = "redmine.account.analytic.line" diff --git a/connector_redmine/components/time_entry/mapper.py b/connector_redmine/components/time_entry/mapper.py new file mode 100644 index 0000000..996265d --- /dev/null +++ b/connector_redmine/components/time_entry/mapper.py @@ -0,0 +1,133 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import logging + +from odoo.addons.component.core import Component +from odoo.addons.connector.components.mapper import mapping +from odoo.addons.connector.exception import MappingError +from odoo.tools.translate import _ + +_logger = logging.getLogger(__name__) + + +class RedmineTimeEntryImportMapper(Component): + _name = "redmine.account.analytic.line.mapper" + _inherit = "redmine.import.mapper" + _apply_on = "redmine.account.analytic.line" + + direct = [ + ("entry_id", "redmine_id"), + ] + + @mapping + def date(self, record): + return {"date": record["time_entry"].spent_on} + + @mapping + def last_update(self, record): + return {"last_update": record["updated_on"]} + + @mapping + def name(self, record): + time_entry = record["time_entry"] + comment = "[#" + str(time_entry.issue.id) + comment += ( + " - " + time_entry.activity.name if hasattr(time_entry, "activity") else "" + ) + comment += "] " + if hasattr(time_entry, "comments"): + # if isinstance(time_entry.comments, unicode): + # 'unicode' to 'str' for port to v12 + if isinstance(time_entry.comments, str): + comment += time_entry.comments + else: + comment += str(time_entry.comments) + return {"name": comment} + + @mapping + def project_id(self, record): + project = self.env["project.project"].search( + [ + ("name", "=", record["contract_ref"]), + ] + ) + if not project: + raise MappingError( + _("No Odoo project with name '%s' exists.") % record["contract_ref"] + ) + if len(project) > 1: + raise MappingError( + _("There are more Odoo projects with name '%s'.") + % record["contract_ref"] + ) + res = { + "project_id": project.id, + "account_id": project.analytic_account_id.id, + } + return res + + @mapping + def redmine_time_entry_id(self, record): + return {"redmine_time_entry_id": record["redmine_id"]} + + @mapping + def task_id(self, record): + time_entry = record["time_entry"] + issue = time_entry.issue + issue_binder = self.binder_for("redmine.issue") + issue_binding = issue_binder.to_internal(issue.id) + res = { + "task_id": issue_binding.odoo_id.id if issue_binding else False, + 'issue_id': issue_binding.id if issue_binding else False, + } + return res + + @mapping + def unit_amount(self, record): + return {"unit_amount": record["time_entry"].hours} + + @mapping + def user_id(self, record): + user_obj = self.env["res.users"] + user = None + + login = record["user_login"] if "user_login" in record else "" + name = record["user"].name if "user" in record else "" + + if login: + user = user_obj.search([("login", "=", login)]) + + if not user: + user = user_obj.search([("name", "=", name)]) + + if not user: + _logger.warning( + _( + "There is no user in Odoo with login name: '%s' or name: '%s'.\n" + "The project's default user will be set for the time entry." + ) + % (login, name) + ) + + project_id = self.project_id(record)["project_id"] + user = ( + self.env["project.project"].browse(project_id).default_redmine_user + ) + + if not user: + raise MappingError( + _( + "There is no user in Odoo with login name: '%s' or name: '%s'.\n" + "And there is also no default Redmine user set for this project." + ) + % (login, name) + ) + + if len(user) > 1: + raise MappingError( + _( + "There is more than one user in Odoo with login name: '%s' or name: '%s'." + ) + % (login, name) + ) + return {"user_id": user.id} diff --git a/connector_redmine/components/time_entry/synchronizer.py b/connector_redmine/components/time_entry/synchronizer.py new file mode 100644 index 0000000..0a95b65 --- /dev/null +++ b/connector_redmine/components/time_entry/synchronizer.py @@ -0,0 +1,106 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from datetime import datetime + +from odoo.addons.component.core import Component +from odoo.addons.connector.exception import MappingError +from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT + + +class TimeEntryBatchImportSynchronizer(Component): + + _name = "redmine.account.analytic.line.batch.importer" + _inherit = "redmine.importer" + _usage = "batch.importer" + _apply_on = "redmine.account.analytic.line" + + def _import_record(self, external_id): + """ Delay the import of the records""" + delayable = self.model.with_delay() + delayable.import_record(self.backend_record, external_id) + + def run(self, filters=None, options=None): + """ + Run the synchronization for all users, using the connector crons. + """ + updated_from = datetime.strptime( + self.backend_record.time_entry_last_update, DEFAULT_SERVER_DATETIME_FORMAT + ) + record_ids = self.backend_adapter.search(updated_from, filters) + for record_id in record_ids: + self._import_record(record_id) + + def run_single_user(self, filters=None, options=None): + """ + Run the synchronization for a single user without using the + connector crons. + All entries with mapping error will be returned in a list + so that the user is notified. This prevents blocking the + import of every timesheets when, for instance, a project + in redmine is not correctly mapped to an analytic account + in Odoo. + :return: list of mapping errors + """ + if options is None: + options = {} + + options["single_user"] = True + + updated_from = datetime.strptime( + self.backend_record.time_entry_last_update, DEFAULT_SERVER_DATETIME_FORMAT + ) + + login = filters.pop("login") + user_id = self.backend_adapter.search_user(login) + filters["user_id"] = user_id + + record_ids = self.backend_adapter.search(updated_from, filters) + + mapping_errors = [] + + for record_id in record_ids: + try: + self._import_record(record_id) + except MappingError as err: + mapping_errors.append(err) + + return mapping_errors + + +class TimeEntryBatchDeleterSynchronizer(Component): + + _name = "redmine.account.analytic.line.batch.deleter" + _inherit = "redmine.importer" + _usage = "batch.deleter" + _apply_on = "redmine.account.analytic.line" + + def get_all_records(self, filters=None): + return self.backend_adapter.search(None, filters) + + +class TimeEntryImportSynchronizer(Component): + + _name = "redmine.account.analytic.line.importer" + _inherit = "redmine.importer" + _usage = "record.importer" + _apply_on = "redmine.account.analytic.line" + + def run(self, record_id, options=None): + """ + Update the last synchronization date on the backend record + The schedule date of the import batch in Odoo can not be + used because the time zone can be different in both systems. + Each time a batch is imported, we take the record with the + highest value for updated_on. + """ + if options is None: + options = {} + + super(TimeEntryImportSynchronizer, self).run(record_id) + + backend = self.backend_record + + if self.updated_on > backend.time_entry_last_update and not options.get( + "single_user", False + ): + backend.write({"time_entry_last_update": self.updated_on}) diff --git a/connector_redmine/data/cron_import_issue.xml b/connector_redmine/data/cron_import_issue.xml new file mode 100644 index 0000000..63660db --- /dev/null +++ b/connector_redmine/data/cron_import_issue.xml @@ -0,0 +1,18 @@ + + + + + + Import issues from all projects + 6 + hours + -1 + + + model._run_import_issues() + code + + + + + \ No newline at end of file diff --git a/connector_redmine/i18n/connector_redmine.pot b/connector_redmine/i18n/connector_redmine.pot new file mode 100644 index 0000000..2ccdcba --- /dev/null +++ b/connector_redmine/i18n/connector_redmine.pot @@ -0,0 +1,1075 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * connector_redmine +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-07-05 14:51+0000\n" +"PO-Revision-Date: 2021-07-05 14:51+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: connector_redmine +#: code:addons/connector_redmine/models/redmine_backend.py:16 +#, python-format +msgid "1.3 and higher" +msgstr "" + +#. module: connector_redmine +#: code:addons/connector_redmine/components/base/adapter.py:59 +#, python-format +msgid "A network error caused the failure of the job: %s" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__access_warning +msgid "Access warning" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__message_needaction +msgid "Action Needed" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__active +msgid "Active" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__activity_ids +msgid "Activities" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__activity_state +msgid "Activity State" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__allow_timesheets +msgid "Allow timesheets" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__amount +msgid "Amount" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__account_id +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__analytic_account_active +msgid "Analytic Account" +msgstr "" + +#. module: connector_redmine +#: model:ir.model,name:connector_redmine.model_account_analytic_line +msgid "Analytic Line" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_project_task__user_id +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__user_id +msgid "Assigned to" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__date_assign +msgid "Assigning Date" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__message_attachment_count +msgid "Attachment Count" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__attachment_ids +msgid "Attachment that don't come from message." +msgstr "" + +#. module: connector_redmine +#: model:ir.ui.menu,name:connector_redmine.menu_redmine_backend +msgid "Backends" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__subtask_project_id +msgid "Choosing a sub-tasks project will both enable sub-tasks and set their default project (possibly the project itself)" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__color +msgid "Color Index" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__company_id +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__company_id +msgid "Company" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__total_hours_spent +msgid "Computed as: Time Spent + Sub-tasks Hours." +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__subtask_planned_hours +msgid "Computed using sum of hours planned of all subtasks created from main task. Usually these hours are less or equal to the Planned Hours (of main task)." +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__effective_hours +msgid "Computed using the sum of the task work done." +msgstr "" + +#. module: connector_redmine +#: code:addons/connector_redmine/models/redmine_backend.py:64 +#: code:addons/connector_redmine/models/redmine_backend.py:109 +#, python-format +msgid "Connection test succeeded. Everything seems properly set up." +msgstr "" + +#. module: connector_redmine +#: code:addons/connector_redmine/models/redmine_backend.py:78 +#, python-format +msgid "Connection to Redmine failed: %s" +msgstr "" + +#. module: connector_redmine +#: code:addons/connector_redmine/models/redmine_backend.py:61 +#, python-format +msgid "Could not connect to Redmine." +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__displayed_image_id +msgid "Cover Image" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__create_uid +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_backend__create_uid +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__create_uid +msgid "Created by" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__create_date +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_backend__create_date +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__create_date +msgid "Created on" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__currency_id +msgid "Currency" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__partner_id +msgid "Customer" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__access_url +msgid "Customer Portal URL" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__date +msgid "Date" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__date_deadline +msgid "Deadline" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_project_project__default_redmine_user +msgid "Default Redmine User" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__department_id +msgid "Department" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__name +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__description +msgid "Description" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__display_name +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_backend__display_name +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_binding__display_name +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__display_name +msgid "Display Name" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_project_task__progress +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__progress +msgid "Display progress of current task." +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_project_project__last_imported_on +msgid "During last issue import this was the date and time of the last updated issue." +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_project_task__last_update +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__last_update +msgid "During last issue import this was the date and time this issue was last updated in Redmine." +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_account_analytic_line__last_update +#: model:ir.model.fields,help:connector_redmine.field_redmine_account_analytic_line__last_update +msgid "During last issue import this was the date and time this time entry was last updated in Redmine." +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__email_from +msgid "Email" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__employee_id +msgid "Employee" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__date_end +msgid "Ending Date" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__message_follower_ids +msgid "Followers" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__message_channel_ids +msgid "Followers (Channels)" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__message_partner_ids +msgid "Followers (Partners)" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__sequence +msgid "Gives the sequence order when displaying a list of tasks." +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__group_id +msgid "Group" +msgstr "" + +#. module: connector_redmine +#: selection:project.task,priority:0 +msgid "High" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__effective_hours +msgid "Hours Spent" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__id +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_backend__id +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_binding__id +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__id +msgid "ID" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__redmine_id +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_binding__redmine_id +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__redmine_id +msgid "ID in Redmine" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__message_unread +msgid "If checked new messages require your attention." +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__message_needaction +msgid "If checked, new messages require your attention." +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__message_has_error +msgid "If checked, some messages have a delivery error." +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__analytic_account_active +msgid "If the active field is set to False, it will allow you to hide the account without removing it." +msgstr "" + +#. module: connector_redmine +#: selection:project.task,priority:0 +msgid "Immediately" +msgstr "" + +#. module: connector_redmine +#: model_terms:ir.ui.view,arch_db:connector_redmine.edit_project_redmine +msgid "Import issues from Redmine" +msgstr "" + +#. module: connector_redmine +#: model:ir.actions.server,name:connector_redmine.ir_cron_import_issue_ir_actions_server +#: model:ir.cron,cron_name:connector_redmine.ir_cron_import_issue +#: model:ir.cron,name:connector_redmine.ir_cron_import_issue +msgid "Import issues from all projects" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_project_task__planned_hours +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__planned_hours +msgid "Initially Planned Hours" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__message_is_follower +msgid "Is Follower" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_project_task__planned_hours +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__planned_hours +msgid "It is the time planned to achieve the task. If this document has sub-tasks, it means the time needed to achieve this tasks and its childs." +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__legend_blocked +msgid "Kanban Blocked Explanation" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__legend_normal +msgid "Kanban Ongoing Explanation" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__kanban_state +msgid "Kanban State" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__kanban_state_label +msgid "Kanban State Label" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__legend_done +msgid "Kanban Valid Explanation" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_backend__key +msgid "Key" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line____last_update +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_backend____last_update +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_binding____last_update +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue____last_update +msgid "Last Modified on" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__date_last_stage_update +msgid "Last Stage Update" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__sync_date +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_binding__sync_date +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__sync_date +msgid "Last Synchronization Date" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__updated_on +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_binding__updated_on +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__updated_on +msgid "Last Update in Redmine" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__write_uid +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_backend__write_uid +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__write_date +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_backend__write_date +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__write_date +msgid "Last Updated on" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_project_project__last_imported_on +#: model_terms:ir.ui.view,arch_db:connector_redmine.edit_project_redmine +msgid "Last imported on" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_account_analytic_line__last_update +#: model:ir.model.fields,field_description:connector_redmine.field_project_task__last_update +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__last_update +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__last_update +msgid "Last updated on" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_backend__location +msgid "Location" +msgstr "" + +#. module: connector_redmine +#: selection:project.task,priority:0 +msgid "Low" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__message_main_attachment_id +msgid "Main Attachment" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__attachment_ids +msgid "Main Attachments" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__message_has_error +msgid "Message Delivery error" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__message_ids +msgid "Messages" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_backend__name +msgid "Name" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__activity_date_deadline +msgid "Next Activity Deadline" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__activity_summary +msgid "Next Activity Summary" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__activity_type_id +msgid "Next Activity Type" +msgstr "" + +#. module: connector_redmine +#: code:addons/connector_redmine/components/issue/mapper.py:133 +#: code:addons/connector_redmine/components/time_entry/mapper.py:56 +#, python-format +msgid "No Odoo project with name '%s' exists." +msgstr "" + +#. module: connector_redmine +#: code:addons/connector_redmine/models/project.py:90 +#, python-format +msgid "No Redmine backend configured that has this location: %s." +msgstr "" + +#. module: connector_redmine +#: code:addons/connector_redmine/components/base/adapter.py:76 +#, python-format +msgid "No user with login %s found in Redmine." +msgstr "" + +#. module: connector_redmine +#: selection:project.task,priority:0 +msgid "Normal" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__notes +msgid "Notes" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__message_needaction_counter +msgid "Number of Actions" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__message_has_error_counter +msgid "Number of error" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__message_needaction_counter +msgid "Number of messages which requires an action" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__message_unread_counter +msgid "Number of unread messages" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__legend_blocked +msgid "Override the default value displayed for the blocked state for kanban selection, when the task or issue is in that stage." +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__legend_done +msgid "Override the default value displayed for the done state for kanban selection, when the task or issue is in that stage." +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__legend_normal +msgid "Override the default value displayed for the normal state for kanban selection, when the task or issue is in that stage." +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__parent_id +msgid "Parent Task" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__partner_id +msgid "Partner" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_project_task_type__redmine_issue_state +msgid "Please enter which Redmine Issue-State shall be mapped to this task type." +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__access_url +msgid "Portal Access URL" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_project_task__priority +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__priority +msgid "Priority" +msgstr "" + +#. module: connector_redmine +#: model:ir.model,name:connector_redmine.model_project_project +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__project_id +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__project_id +msgid "Project" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__manager_id +msgid "Project Manager" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_backend__proxy +msgid "Proxy" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__unit_amount +msgid "Quantity" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__rating_ids +msgid "Rating" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__rating_last_feedback +msgid "Rating Last Feedback" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__rating_last_image +msgid "Rating Last Image" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__rating_last_value +msgid "Rating Last Value" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__rating_count +msgid "Rating count" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__rating_last_feedback +msgid "Reason of the rating" +msgstr "" + +#. module: connector_redmine +#: code:addons/connector_redmine/components/base/synchronizer.py:74 +#, python-format +msgid "Record does no longer exist in Redmine." +msgstr "" + +#. module: connector_redmine +#: model:ir.ui.menu,name:connector_redmine.menu_redmine_root +msgid "Redmine" +msgstr "" + +#. module: connector_redmine +#: model:ir.actions.act_window,name:connector_redmine.action_redmine_backend +#: model:ir.model,name:connector_redmine.model_redmine_backend +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__backend_id +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_binding__backend_id +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__backend_id +#: model_terms:ir.ui.view,arch_db:connector_redmine.view_redmine_backend_form +#: model_terms:ir.ui.view,arch_db:connector_redmine.view_redmine_backend_tree +msgid "Redmine Backend" +msgstr "" + +#. module: connector_redmine +#: model:ir.model,name:connector_redmine.model_redmine_binding +msgid "Redmine Binding (Abstract)" +msgstr "" + +#. module: connector_redmine +#: model_terms:ir.ui.view,arch_db:connector_redmine.view_redmine_backend_form +msgid "Redmine Configuration" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_account_analytic_line__redmine_time_entry_id +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__redmine_time_entry_id +msgid "Redmine ID" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_project_task__redmine_issue_url +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__issue_id +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__redmine_issue_url +msgid "Redmine Issue" +msgstr "" + +#. module: connector_redmine +#: model:ir.model,name:connector_redmine.model_redmine_issue +msgid "Redmine Issue Binding" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_project_task_type__redmine_issue_state +msgid "Redmine Issue State" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_project_project__redmine_project_url +msgid "Redmine Project" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_backend__contract_ref +msgid "Redmine Project Identification" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__timesheet_ids +msgid "Redmine Time Entries" +msgstr "" + +#. module: connector_redmine +#: model:ir.model,name:connector_redmine.model_redmine_account_analytic_line +msgid "Redmine Time Entry Binding" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_project_task__redmine_user +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__redmine_user +msgid "Redmine User" +msgstr "" + +#. module: connector_redmine +#: code:addons/connector_redmine/models/redmine_backend.py:102 +#, python-format +msgid "Redmine backend configuration error:\n" +"The Redmine project identification field is not assigned in Redmine." +msgstr "" + +#. module: connector_redmine +#: code:addons/connector_redmine/models/redmine_backend.py:95 +#, python-format +msgid "Redmine backend configuration error:\n" +"The Redmine project identification name doesn't exist." +msgstr "" + +#. module: connector_redmine +#: code:addons/connector_redmine/components/base/adapter.py:54 +#, python-format +msgid "Redmine connection Error: Invalid authentications key. (%s)" +msgstr "" + +#. module: connector_redmine +#: code:addons/connector_redmine/components/issue/adapter.py:139 +#: code:addons/connector_redmine/components/time_entry/adapter.py:56 +#, python-format +msgid "Redmine-Issue: %s - The user %s does not exist in Redmine anymore." +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__remaining_hours +msgid "Remaining Hours" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__activity_user_id +msgid "Responsible User" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__access_token +msgid "Security Token" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__sequence +msgid "Sequence" +msgstr "" + +#. module: connector_redmine +#: code:addons/connector_redmine/models/project.py:60 +#, python-format +msgid "Some issues were not imported from Redmine." +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__stage_id +msgid "Stage" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__date_start +msgid "Starting Date" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__activity_state +msgid "Status based on activities\n" +"Overdue: Due date is already passed\n" +"Today: Activity date is today\n" +"Planned: Future activities." +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__subtask_project_id +msgid "Sub-task Project" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__subtask_count +msgid "Sub-task count" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__child_ids +msgid "Sub-tasks" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__subtask_effective_hours +msgid "Sub-tasks Hours Spent" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__subtask_planned_hours +msgid "Subtasks" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__subtask_effective_hours +msgid "Sum of actually spent hours on the subtask(s)" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__tag_ids +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__tag_ids +msgid "Tags" +msgstr "" + +#. module: connector_redmine +#: model:ir.model,name:connector_redmine.model_project_task +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__task_id +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__odoo_id +msgid "Task" +msgstr "" + +#. module: connector_redmine +#: model:ir.model,name:connector_redmine.model_project_task_type +msgid "Task Stage" +msgstr "" + +#. module: connector_redmine +#: model_terms:ir.ui.view,arch_db:connector_redmine.view_redmine_backend_form +msgid "Test Authentication" +msgstr "" + +#. module: connector_redmine +#: model_terms:ir.ui.view,arch_db:connector_redmine.view_redmine_backend_form_inherit +msgid "Test Redmine Project Identification" +msgstr "" + +#. module: connector_redmine +#: code:addons/connector_redmine/components/issue/adapter.py:126 +#: code:addons/connector_redmine/components/time_entry/adapter.py:46 +#, python-format +msgid "The field %(field)s is not set in Redmine for project %(project)s." +msgstr "" + +#. module: connector_redmine +#: code:addons/connector_redmine/components/issue/mapper.py:137 +#: code:addons/connector_redmine/components/time_entry/mapper.py:60 +#, python-format +msgid "There are more Odoo projects with name '%s'." +msgstr "" + +#. module: connector_redmine +#: code:addons/connector_redmine/components/issue/mapper.py:171 +#, python-format +msgid "There are more project task types for the given project '%s' that match the Redmine state '%s'.\n" +"Check for example these types: '%s' and '%s'." +msgstr "" + +#. module: connector_redmine +#: code:addons/connector_redmine/components/issue/adapter.py:93 +#, python-format +msgid "There is more than one Redmine project with given name: '%s' or URL: '%s'." +msgstr "" + +#. module: connector_redmine +#: code:addons/connector_redmine/components/issue/mapper.py:211 +#: code:addons/connector_redmine/components/time_entry/mapper.py:128 +#, python-format +msgid "There is more than one user in Odoo with login name: '%s' or name: '%s'." +msgstr "" + +#. module: connector_redmine +#: code:addons/connector_redmine/components/issue/adapter.py:71 +#, python-format +msgid "There is no Redmine project URL given to identify correct redmine project." +msgstr "" + +#. module: connector_redmine +#: code:addons/connector_redmine/components/issue/adapter.py:86 +#, python-format +msgid "There is no Redmine project with given name: '%s' or URL: '%s'." +msgstr "" + +#. module: connector_redmine +#: code:addons/connector_redmine/components/issue/mapper.py:160 +#, python-format +msgid "There is no project task type for the given project '%s' that matches the Redmine state '%s'.\n" +"Add the Redmine state to an existing project task type or create a new one.\n" +"Make also sure that the task type is available for this project." +msgstr "" + +#. module: connector_redmine +#: code:addons/connector_redmine/components/time_entry/mapper.py:119 +#, python-format +msgid "There is no user in Odoo with login name: '%s' or name: '%s'.\n" +"And there is also no default Redmine user set for this project." +msgstr "" + +#. module: connector_redmine +#: code:addons/connector_redmine/components/time_entry/mapper.py:105 +#, python-format +msgid "There is no user in Odoo with login name: '%s' or name: '%s'.\n" +"The project's default user will be set for the time entry." +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__email_cc +msgid "These email addresses will be added to the CC field of all inbound\n" +" and outbound emails for this record before being sent. Separate multiple email addresses with a comma" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__email_from +msgid "These people will receive email." +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_project_task__redmine_user +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__redmine_user +msgid "This is the name of the assigned Redmine User. It is set, if the Redmine user is no Odoo user." +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_backend__contract_ref +msgid "This is the name of the field added in Redmine used to relate a Redmine project to an Odoo project.\n" +"Each Redmine project must have an unique value for this attribute." +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_project_project__default_redmine_user +msgid "This is the user who will be set to tasks and account analytic lines if the Redmine user does not match any Odoo user, when importing issues from Redmine." +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__odoo_id +msgid "Timesheet" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__allow_timesheets +msgid "Timesheets can be logged on this task." +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__name +msgid "Title" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__total_hours_spent +msgid "Total Hours" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__remaining_hours +msgid "Total remaining time, can be re-estimated periodically by the assignee of the task." +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__product_uom_id +msgid "Unit of Measure" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__message_unread +msgid "Unread Messages" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__message_unread_counter +msgid "Unread Messages Counter" +msgstr "" + +#. module: connector_redmine +#: selection:project.task,priority:0 +msgid "Urgent" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__user_id +msgid "User" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__user_email +msgid "User Email" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_backend__verify_ssl +msgid "Verify SSL" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_backend__version +msgid "Version" +msgstr "" + +#. module: connector_redmine +#: selection:project.task,priority:0 +msgid "Very High" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__email_cc +msgid "Watchers Emails" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__website_message_ids +msgid "Website Messages" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__website_message_ids +msgid "Website communication history" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_project_task__progress +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__progress +msgid "Working Time Recorded" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__working_days_open +msgid "Working days to assign" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__working_days_close +msgid "Working days to close" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__working_hours_open +msgid "Working hours to assign" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__working_hours_close +msgid "Working hours to close" +msgstr "" + +#. module: connector_redmine +#: model_terms:ir.ui.view,arch_db:connector_redmine.view_redmine_backend_form +msgid "e.g. http://proxy:8081" +msgstr "" + +#. module: connector_redmine +#: model_terms:ir.ui.view,arch_db:connector_redmine.view_redmine_backend_form +msgid "e.g. http://redmine.url/" +msgstr "" + +#. module: connector_redmine +#: model_terms:ir.ui.view,arch_db:connector_redmine.edit_project_redmine +msgid "e.g. http://redmine.url/projects/projectname/" +msgstr "" + diff --git a/connector_redmine/i18n/de.po b/connector_redmine/i18n/de.po new file mode 100644 index 0000000..f94f582 --- /dev/null +++ b/connector_redmine/i18n/de.po @@ -0,0 +1,1087 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * connector_redmine +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-07-05 14:51+0000\n" +"PO-Revision-Date: 2021-07-05 14:51+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: connector_redmine +#: code:addons/connector_redmine/models/redmine_backend.py:16 +#, python-format +msgid "1.3 and higher" +msgstr "1.3 und höher" + +#. module: connector_redmine +#: code:addons/connector_redmine/components/base/adapter.py:59 +#, python-format +msgid "A network error caused the failure of the job: %s" +msgstr "Ein Netzwerkfehler hat einen Fehler im Job: %s verursacht." + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__access_warning +msgid "Access warning" +msgstr "Zugriffswarnung" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__message_needaction +msgid "Action Needed" +msgstr "Aktion notwendig" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__active +msgid "Active" +msgstr "Aktiv" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__activity_ids +msgid "Activities" +msgstr "Aktivitäten" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__activity_state +msgid "Activity State" +msgstr "Aktivitätsstatus" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__allow_timesheets +msgid "Allow timesheets" +msgstr "Zeitnachweise erlauben" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__amount +msgid "Amount" +msgstr "Betrag" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__account_id +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__analytic_account_active +msgid "Analytic Account" +msgstr "Analysekonto" + +#. module: connector_redmine +#: model:ir.model,name:connector_redmine.model_account_analytic_line +msgid "Analytic Line" +msgstr "Analytischer Buchungssatz" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_project_task__user_id +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__user_id +msgid "Assigned to" +msgstr "Zugewiesen an" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__date_assign +msgid "Assigning Date" +msgstr "Zuweisungsdatum" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__message_attachment_count +msgid "Attachment Count" +msgstr "# Anhänge" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__attachment_ids +msgid "Attachment that don't come from message." +msgstr "Anhang, der nicht von der Nachricht stammt." + +#. module: connector_redmine +#: model:ir.ui.menu,name:connector_redmine.menu_redmine_backend +msgid "Backends" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__subtask_project_id +msgid "Choosing a sub-tasks project will both enable sub-tasks and set their default project (possibly the project itself)" +msgstr "Indem Sie ein Unteraufgaben-Projekt auswählen, werden Unteraufgaben aktiviert und es wird deren Standard-Projekt festgelegt (eventuell das Projekt selbst)." + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__color +msgid "Color Index" +msgstr "Farbkennzeichnung" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__company_id +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__company_id +msgid "Company" +msgstr "Unternehmen" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__total_hours_spent +msgid "Computed as: Time Spent + Sub-tasks Hours." +msgstr "Berechnet als: Zeitaufwand + Stunden für Unteraufgaben." + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__subtask_planned_hours +msgid "Computed using sum of hours planned of all subtasks created from main task. Usually these hours are less or equal to the Planned Hours (of main task)." +msgstr "Berechnet aus der Summe der geplanten Stunden aller Teilaufgaben, die aus der Hauptaufgabe erstellt wurden. In der Regel sind diese Stunden kleiner oder gleich den geplanten Stunden (der Hauptaufgabe)." + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__effective_hours +msgid "Computed using the sum of the task work done." +msgstr "Berechnet auf Basis der Summe der abgearbeiteten Aufgaben." + +#. module: connector_redmine +#: code:addons/connector_redmine/models/redmine_backend.py:64 +#: code:addons/connector_redmine/models/redmine_backend.py:109 +#, python-format +msgid "Connection test succeeded. Everything seems properly set up." +msgstr "Verbindungstest war erfolgreich. Alles scheint korrekt eingerichtet zu sein." + +#. module: connector_redmine +#: code:addons/connector_redmine/models/redmine_backend.py:78 +#, python-format +msgid "Connection to Redmine failed: %s" +msgstr "Die Verbindung zu Redmine ist fehlgeschlagen: %s" + +#. module: connector_redmine +#: code:addons/connector_redmine/models/redmine_backend.py:61 +#, python-format +msgid "Could not connect to Redmine." +msgstr "Es konnte keine Verbindung zur Redmine aufgebaut werden." + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__displayed_image_id +msgid "Cover Image" +msgstr "Titelbild" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__create_uid +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_backend__create_uid +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__create_uid +msgid "Created by" +msgstr "Erstellt von" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__create_date +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_backend__create_date +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__create_date +msgid "Created on" +msgstr "Erstellt am" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__currency_id +msgid "Currency" +msgstr "Währung" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__partner_id +msgid "Customer" +msgstr "Kunde" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__access_url +msgid "Customer Portal URL" +msgstr "Kundenportal-URL" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__date +msgid "Date" +msgstr "Datum" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__date_deadline +msgid "Deadline" +msgstr "Frist" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_project_project__default_redmine_user +msgid "Default Redmine User" +msgstr "Standard-Redmine-Benutzer" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__department_id +msgid "Department" +msgstr "Abteilung" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__name +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__description +msgid "Description" +msgstr "Beschreibung" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__display_name +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_backend__display_name +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_binding__display_name +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__display_name +msgid "Display Name" +msgstr "Anzeigename" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_project_task__progress +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__progress +msgid "Display progress of current task." +msgstr "Das Feld zeigt den Fortschritt der Aufgabe an." + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_project_project__last_imported_on +msgid "During last issue import this was the date and time of the last updated issue." +msgstr "Während des letzten Ticket-Imports war dies das Datum und die Zeit, zu der das Ticket in Redmine zuletzt geändert wurde." + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_project_task__last_update +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__last_update +msgid "During last issue import this was the date and time this issue was last updated in Redmine." +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_account_analytic_line__last_update +#: model:ir.model.fields,help:connector_redmine.field_redmine_account_analytic_line__last_update +msgid "During last issue import this was the date and time this time entry was last updated in Redmine." +msgstr "Während des letzten Ticket-Imports war dies das Datum und die Zeit, zu der das Ticket in Redmine zuletzt geändert wurde." + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__email_from +msgid "Email" +msgstr "E-Mail" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__employee_id +msgid "Employee" +msgstr "Mitarbeiter" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__date_end +msgid "Ending Date" +msgstr "Enddatum" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__message_follower_ids +msgid "Followers" +msgstr "Abonnenten" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__message_channel_ids +msgid "Followers (Channels)" +msgstr "Abonnenten (Kanäle)" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__message_partner_ids +msgid "Followers (Partners)" +msgstr "Abonnenten (Partner)" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__sequence +msgid "Gives the sequence order when displaying a list of tasks." +msgstr "Die Reihenfolge der Liste der Aufgaben." + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__group_id +msgid "Group" +msgstr "Gruppe" + +#. module: connector_redmine +#: selection:project.task,priority:0 +msgid "High" +msgstr "Hoch" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__effective_hours +msgid "Hours Spent" +msgstr "Geleistete Stunden" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__id +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_backend__id +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_binding__id +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__id +msgid "ID" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__redmine_id +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_binding__redmine_id +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__redmine_id +msgid "ID in Redmine" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__message_unread +msgid "If checked new messages require your attention." +msgstr "Falls markiert, benötigen neue Nachrichten Ihre Kenntnisnahme." + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__message_needaction +msgid "If checked, new messages require your attention." +msgstr "Falls markiert, benötigen neue Nachrichten Ihre Kenntnisnahme." + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__message_has_error +msgid "If checked, some messages have a delivery error." +msgstr "Das Senden mancher Nachrichten ist fehlgeschlagen wenn dieses Fenster angekreuzt ist." + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__analytic_account_active +msgid "If the active field is set to False, it will allow you to hide the account without removing it." +msgstr "Mit Aktivierung wird die Anzeige für das Finanzkonto verborgen, ohne es löschen zu müssen." + +#. module: connector_redmine +#: selection:project.task,priority:0 +msgid "Immediately" +msgstr "" + +#. module: connector_redmine +#: model_terms:ir.ui.view,arch_db:connector_redmine.edit_project_redmine +msgid "Import issues from Redmine" +msgstr "Redmine-Tickets importieren" + +#. module: connector_redmine +#: model:ir.actions.server,name:connector_redmine.ir_cron_import_issue_ir_actions_server +#: model:ir.cron,cron_name:connector_redmine.ir_cron_import_issue +#: model:ir.cron,name:connector_redmine.ir_cron_import_issue +msgid "Import issues from all projects" +msgstr "Tickets von allen Projekten importieren" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_project_task__planned_hours +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__planned_hours +msgid "Initially Planned Hours" +msgstr "Ursprünglich geplante Stunden" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__message_is_follower +msgid "Is Follower" +msgstr "Ist ein Abonnent" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_project_task__planned_hours +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__planned_hours +msgid "It is the time planned to achieve the task. If this document has sub-tasks, it means the time needed to achieve this tasks and its childs." +msgstr "Es ist die geplante Zeit, um die Aufgabe zu erfüllen. Wenn dieses Dokument Teilaufgaben hat, bedeutet dies die Zeit, die benötigt wird, um diese Aufgaben und ihre Unteraufgaben zu erfüllen." + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__legend_blocked +msgid "Kanban Blocked Explanation" +msgstr "Erläuterung zu gesperrtem Kanban " + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__legend_normal +msgid "Kanban Ongoing Explanation" +msgstr "Erläuterung zu Kanban in Bearbeitung" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__kanban_state +msgid "Kanban State" +msgstr "Kanban-Status" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__kanban_state_label +msgid "Kanban State Label" +msgstr "Kanban Statuslabel" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__legend_done +msgid "Kanban Valid Explanation" +msgstr "Erläuterung zu gültigem Kanban " + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_backend__key +msgid "Key" +msgstr "Schlüssel" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line____last_update +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_backend____last_update +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_binding____last_update +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue____last_update +msgid "Last Modified on" +msgstr "Zuletzt geändert am" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__date_last_stage_update +msgid "Last Stage Update" +msgstr "Aktuellste Stufenaktualisierung" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__sync_date +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_binding__sync_date +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__sync_date +msgid "Last Synchronization Date" +msgstr "Datum der letzten Synchronisation" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__updated_on +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_binding__updated_on +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__updated_on +msgid "Last Update in Redmine" +msgstr "Zuletzt in Redmine aktualisiert am" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__write_uid +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_backend__write_uid +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__write_uid +msgid "Last Updated by" +msgstr "Zuletzt aktualisiert durch" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__write_date +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_backend__write_date +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__write_date +msgid "Last Updated on" +msgstr "Zuletzt aktualisiert am" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_project_project__last_imported_on +#: model_terms:ir.ui.view,arch_db:connector_redmine.edit_project_redmine +msgid "Last imported on" +msgstr "Zuletzt importiert am" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_account_analytic_line__last_update +#: model:ir.model.fields,field_description:connector_redmine.field_project_task__last_update +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__last_update +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__last_update +msgid "Last updated on" +msgstr "Zuletzt aktualisiert am" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_backend__location +msgid "Location" +msgstr "Redmine-URL" + +#. module: connector_redmine +#: selection:project.task,priority:0 +msgid "Low" +msgstr "Niedrig" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__message_main_attachment_id +msgid "Main Attachment" +msgstr "Hauptanhänge" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__attachment_ids +msgid "Main Attachments" +msgstr "Hauptanhänge" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__message_has_error +msgid "Message Delivery error" +msgstr "Error beim senden der Nachricht" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__message_ids +msgid "Messages" +msgstr "Nachrichten" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_backend__name +msgid "Name" +msgstr "Bezeichnung" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__activity_date_deadline +msgid "Next Activity Deadline" +msgstr "Frist für die nächste Aktivität" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__activity_summary +msgid "Next Activity Summary" +msgstr "Zusammenfassung der nächsten Aktivität" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__activity_type_id +msgid "Next Activity Type" +msgstr "Typ der nächsten Aktivität" + +#. module: connector_redmine +#: code:addons/connector_redmine/components/issue/mapper.py:133 +#: code:addons/connector_redmine/components/time_entry/mapper.py:56 +#, python-format +msgid "No Odoo project with name '%s' exists." +msgstr "Es existiert in Odoo kein Projekt mit Namen '%s'." + +#. module: connector_redmine +#: code:addons/connector_redmine/models/project.py:90 +#, python-format +msgid "No Redmine backend configured that has this location: %s." +msgstr "Es existiert kein Redmine-Backend mit dieser URL: %s." + +#. module: connector_redmine +#: code:addons/connector_redmine/components/base/adapter.py:76 +#, python-format +msgid "No user with login %s found in Redmine." +msgstr "Es existiert kein User mit dem Login %s in Redmine." + +#. module: connector_redmine +#: selection:project.task,priority:0 +msgid "Normal" +msgstr "Mittel" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__notes +msgid "Notes" +msgstr "Notizen" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__message_needaction_counter +msgid "Number of Actions" +msgstr "Anzahl der Aktionen" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__message_has_error_counter +msgid "Number of error" +msgstr "Fehlernummer" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__message_needaction_counter +msgid "Number of messages which requires an action" +msgstr "Anzahl der Nachrichten, die eine Aktion erfordern" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "Anzahl der Nachrichten mit einem Fehler beim Senden." + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__message_unread_counter +msgid "Number of unread messages" +msgstr "Anzahl ungelesener Nachrichten" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__legend_blocked +msgid "Override the default value displayed for the blocked state for kanban selection, when the task or issue is in that stage." +msgstr "Überschreiben Sie den Standard Wert für den Blockade Status in der Kanban Auswahl, wenn die Aufgabe oder der Vorfall in dieser Stufe ist." + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__legend_done +msgid "Override the default value displayed for the done state for kanban selection, when the task or issue is in that stage." +msgstr "Überschreiben Sie den Standard Wert für den Erledigt Status in der Kanban Auswahl, wenn die Aufgabe oder der Vorfall in dieser Stufe ist." + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__legend_normal +msgid "Override the default value displayed for the normal state for kanban selection, when the task or issue is in that stage." +msgstr "Überschreiben Sie den Standard Wert für den Normal Status in der Kanban Auswahl, wenn die Aufgabe oder der Vorfall in dieser Stufe ist." + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__parent_id +msgid "Parent Task" +msgstr "Hauptaufgabe" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__partner_id +msgid "Partner" +msgstr "Partner" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_project_task_type__redmine_issue_state +msgid "Please enter which Redmine Issue-State shall be mapped to this task type." +msgstr "Geben Sie hier den Redmine-Ticketstatus ein, der dieser Projektstufe entsprechen soll." + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__access_url +msgid "Portal Access URL" +msgstr "Portalzugriff-URL" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_project_task__priority +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__priority +msgid "Priority" +msgstr "Priorität" + +#. module: connector_redmine +#: model:ir.model,name:connector_redmine.model_project_project +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__project_id +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__project_id +msgid "Project" +msgstr "Projekt" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__manager_id +msgid "Project Manager" +msgstr "Projektmanager" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_backend__proxy +msgid "Proxy" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__unit_amount +msgid "Quantity" +msgstr "Menge" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__rating_ids +msgid "Rating" +msgstr "Bewertung" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__rating_last_feedback +msgid "Rating Last Feedback" +msgstr "Bewertung letztes Feedback" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__rating_last_image +msgid "Rating Last Image" +msgstr "Bewertung letztes Bild" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__rating_last_value +msgid "Rating Last Value" +msgstr "Bewertung letzten Werte" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__rating_count +msgid "Rating count" +msgstr "Bewertung Anzahl" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__rating_last_feedback +msgid "Reason of the rating" +msgstr "Begründung der Bewertung" + +#. module: connector_redmine +#: code:addons/connector_redmine/components/base/synchronizer.py:74 +#, python-format +msgid "Record does no longer exist in Redmine." +msgstr "Der Datensatz existiert nicht mehr in Redmine." + +#. module: connector_redmine +#: model:ir.ui.menu,name:connector_redmine.menu_redmine_root +msgid "Redmine" +msgstr "" + +#. module: connector_redmine +#: model:ir.actions.act_window,name:connector_redmine.action_redmine_backend +#: model:ir.model,name:connector_redmine.model_redmine_backend +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__backend_id +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_binding__backend_id +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__backend_id +#: model_terms:ir.ui.view,arch_db:connector_redmine.view_redmine_backend_form +#: model_terms:ir.ui.view,arch_db:connector_redmine.view_redmine_backend_tree +msgid "Redmine Backend" +msgstr "Redmine-Backend" + +#. module: connector_redmine +#: model:ir.model,name:connector_redmine.model_redmine_binding +msgid "Redmine Binding (Abstract)" +msgstr "Redmine-Verbindung (Abtrakt)" + +#. module: connector_redmine +#: model_terms:ir.ui.view,arch_db:connector_redmine.view_redmine_backend_form +msgid "Redmine Configuration" +msgstr "Redmine-Konfiguration" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_account_analytic_line__redmine_time_entry_id +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__redmine_time_entry_id +msgid "Redmine ID" +msgstr "Redmine-ID" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_project_task__redmine_issue_url +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__issue_id +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__redmine_issue_url +msgid "Redmine Issue" +msgstr "Redmine-Ticket" + +#. module: connector_redmine +#: model:ir.model,name:connector_redmine.model_redmine_issue +msgid "Redmine Issue Binding" +msgstr "Redmine-Ticket-Verbindung" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_project_task_type__redmine_issue_state +msgid "Redmine Issue State" +msgstr "Redmine-Ticketstatus" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_project_project__redmine_project_url +msgid "Redmine Project" +msgstr "Redmine-Projekt" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_backend__contract_ref +msgid "Redmine Project Identification" +msgstr "Redmine-Projektidentifikation" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__timesheet_ids +msgid "Redmine Time Entries" +msgstr "Redmine-Zeiterfassungen" + +#. module: connector_redmine +#: model:ir.model,name:connector_redmine.model_redmine_account_analytic_line +msgid "Redmine Time Entry Binding" +msgstr "Redmine-Zeiterfassungsverbindung" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_project_task__redmine_user +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__redmine_user +msgid "Redmine User" +msgstr "Redmine-Benutzer" + +#. module: connector_redmine +#: code:addons/connector_redmine/models/redmine_backend.py:102 +#, python-format +msgid "Redmine backend configuration error:\n" +"The Redmine project identification field is not assigned in Redmine." +msgstr "Redmine-Backend-Konfigurationsfehler:\n" +"Das Feld für die Redmine-Projektidentikation ist im Redmine nicht ausgefüllt." + +#. module: connector_redmine +#: code:addons/connector_redmine/models/redmine_backend.py:95 +#, python-format +msgid "Redmine backend configuration error:\n" +"The Redmine project identification name doesn't exist." +msgstr "Redmine-Backend-Konfigurationsfehler:\n" +"Der Name des Feldes für die Redmine-Projektidentikation existiert im Redmine nicht." + +#. module: connector_redmine +#: code:addons/connector_redmine/components/base/adapter.py:54 +#, python-format +msgid "Redmine connection Error: Invalid authentications key. (%s)" +msgstr "Redmine-Verbindungsfehler: Ungültiger Authentifizierungsschlüssel. (%s)" + +#. module: connector_redmine +#: code:addons/connector_redmine/components/issue/adapter.py:139 +#: code:addons/connector_redmine/components/time_entry/adapter.py:56 +#, python-format +msgid "Redmine-Issue: %s - The user %s does not exist in Redmine anymore." +msgstr "Redmine-Ticket: %s - Der Benutzer %s existiert in Redmine nicht mehr." + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__remaining_hours +msgid "Remaining Hours" +msgstr "Verbleibende Stunden" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__activity_user_id +msgid "Responsible User" +msgstr "Verantwortlicher Benutzer" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__access_token +msgid "Security Token" +msgstr "Sicherheitstoken" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__sequence +msgid "Sequence" +msgstr "Nummernfolge" + +#. module: connector_redmine +#: code:addons/connector_redmine/models/project.py:60 +#, python-format +msgid "Some issues were not imported from Redmine." +msgstr "Einige Tickets wurden nicht von Redmine importiert." + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__stage_id +msgid "Stage" +msgstr "Stufe" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__date_start +msgid "Starting Date" +msgstr "Startdatum" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__activity_state +msgid "Status based on activities\n" +"Overdue: Due date is already passed\n" +"Today: Activity date is today\n" +"Planned: Future activities." +msgstr "Der Status hängt von den Aktivitäten ab.\n" +"Überfällig: Das Fälligkeitsdatum der Aktivität ist überschritten.\n" +"Heute: Die Aktivität findet heute statt.\n" +"Geplant: Die Aktivitäten findet in der Zukunft statt." + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__subtask_project_id +msgid "Sub-task Project" +msgstr "Unteraufgaben-Projekt" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__subtask_count +msgid "Sub-task count" +msgstr "Anzahl Unteraufgaben" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__child_ids +msgid "Sub-tasks" +msgstr "Unteraufgaben" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__subtask_effective_hours +msgid "Sub-tasks Hours Spent" +msgstr "Aufgewendete Stunden für Unteraufgaben" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__subtask_planned_hours +msgid "Subtasks" +msgstr "Teilaufgaben" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__subtask_effective_hours +msgid "Sum of actually spent hours on the subtask(s)" +msgstr "Dies ist die Summe der tatsächlich aufgewendeten Stunden in Unteraufgaben." + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__tag_ids +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__tag_ids +msgid "Tags" +msgstr "Stichwörter" + +#. module: connector_redmine +#: model:ir.model,name:connector_redmine.model_project_task +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__task_id +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__odoo_id +msgid "Task" +msgstr "Aufgabe" + +#. module: connector_redmine +#: model:ir.model,name:connector_redmine.model_project_task_type +msgid "Task Stage" +msgstr "Aufgabenstufe" + +#. module: connector_redmine +#: model_terms:ir.ui.view,arch_db:connector_redmine.view_redmine_backend_form +msgid "Test Authentication" +msgstr "Authentifikation testen" + +#. module: connector_redmine +#: model_terms:ir.ui.view,arch_db:connector_redmine.view_redmine_backend_form_inherit +msgid "Test Redmine Project Identification" +msgstr "Redmine-Projektidentifikation testen" + +#. module: connector_redmine +#: code:addons/connector_redmine/components/issue/adapter.py:126 +#: code:addons/connector_redmine/components/time_entry/adapter.py:46 +#, python-format +msgid "The field %(field)s is not set in Redmine for project %(project)s." +msgstr "Das Feld %(field)s ist in Redmine für das Projekt %(project)s nicht gesetzt." + +#. module: connector_redmine +#: code:addons/connector_redmine/components/issue/mapper.py:137 +#: code:addons/connector_redmine/components/time_entry/mapper.py:60 +#, python-format +msgid "There are more Odoo projects with name '%s'." +msgstr "Es existieren mehrere Odoo-Projekte mit dem Namen '%s'." + +#. module: connector_redmine +#: code:addons/connector_redmine/components/issue/mapper.py:171 +#, python-format +msgid "There are more project task types for the given project '%s' that match the Redmine state '%s'.\n" +"Check for example these types: '%s' and '%s'." +msgstr "Es existieren mehrere Projekt-Stufen für das Projekt '%s' die dem Redmine-Status '%s' entsprechen.\n" +"Prüfen Sie beispielsweise diese Stufen: '%s' und '%s'." + +#. module: connector_redmine +#: code:addons/connector_redmine/components/issue/adapter.py:93 +#, python-format +msgid "There is more than one Redmine project with given name: '%s' or URL: '%s'." +msgstr "Es existieren mehrere Redmine-Projekte mit dem Namen: '%s' oder der URL: '%s'." + +#. module: connector_redmine +#: code:addons/connector_redmine/components/issue/mapper.py:211 +#: code:addons/connector_redmine/components/time_entry/mapper.py:128 +#, python-format +msgid "There is more than one user in Odoo with login name: '%s' or name: '%s'." +msgstr "Es existieren in Odoo mehrere Benutzer mit dem Login-Namen: '%s' oder dem Namen: '%s'." + +#. module: connector_redmine +#: code:addons/connector_redmine/components/issue/adapter.py:71 +#, python-format +msgid "There is no Redmine project URL given to identify correct redmine project." +msgstr "Es wurde keine Redmine-Projekt-URL angegeben, um das richtige Redmine-Projekt zu identifizieren." + +#. module: connector_redmine +#: code:addons/connector_redmine/components/issue/adapter.py:86 +#, python-format +msgid "There is no Redmine project with given name: '%s' or URL: '%s'." +msgstr "Es existiert kein Redmine-Projekt mit dem Namen: '%s' oder der URL: '%s'." + +#. module: connector_redmine +#: code:addons/connector_redmine/components/issue/mapper.py:160 +#, python-format +msgid "There is no project task type for the given project '%s' that matches the Redmine state '%s'.\n" +"Add the Redmine state to an existing project task type or create a new one.\n" +"Make also sure that the task type is available for this project." +msgstr "Es existiert keine Projekt-Stufe für das Projekt '%s', das dem Redmine-Status '%s' entspricht.\n" +"Fügen Sie den Redmine-Status zu existierenden Projekt-Stufen hinzu oder erstellen Sie eine neue Stufe.\n" +"Beachten Sie dabei auch, dass die Projekt-Stufe für das Projekt verwendet wird." + +#. module: connector_redmine +#: code:addons/connector_redmine/components/time_entry/mapper.py:119 +#, python-format +msgid "There is no user in Odoo with login name: '%s' or name: '%s'.\n" +"And there is also no default Redmine user set for this project." +msgstr "Es existiert in Odoo kein Benutzer mit dem Login-Namen: '%s' oder dem Namen: '%s'.\n" +"Und für das Projekt ist auch kein Standard-Redmine-Benutzer angegeben." + +#. module: connector_redmine +#: code:addons/connector_redmine/components/time_entry/mapper.py:105 +#, python-format +msgid "There is no user in Odoo with login name: '%s' or name: '%s'.\n" +"The project's default user will be set for the time entry." +msgstr "Es existiert in Odoo kein Benutzer mit dem Login-Namen: '%s' oder dem Namen: '%s'.\n" +"Der Standard-Redmine-Benutzer des Projekts wird an dem Zeiteintrag gesetzt." + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__email_cc +msgid "These email addresses will be added to the CC field of all inbound\n" +" and outbound emails for this record before being sent. Separate multiple email addresses with a comma" +msgstr "Diese E-Mail-Adressen werden werden zum CC-Feld aller ein-\n" +"  und ausgehenden E-Mails für diesen Datensatz vor dem Senden hinzugefügt. Mehrere E-Mail-Adressen mit einem Komma abtrennen" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__email_from +msgid "These people will receive email." +msgstr "Diese Personen erhalten E-Mails" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_project_task__redmine_user +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__redmine_user +msgid "This is the name of the assigned Redmine User. It is set, if the Redmine user is no Odoo user." +msgstr "Das ist der Name des zugewiesenen Redmine-Benutzers. Wenn es ausgefüllt ist, gibt es den Redmine User nicht in Odoo." + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_backend__contract_ref +msgid "This is the name of the field added in Redmine used to relate a Redmine project to an Odoo project.\n" +"Each Redmine project must have an unique value for this attribute." +msgstr "Das ist der Name des Feldes, das im Redmine hinzugefügt wurde, um das Redmine-Projekt mit einem Odoo-Projekt zu verbinden.\n" +"Jedes Redmine-Projekt muss einen eindeutigen Wert für dieses Attribut haben." + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_project_project__default_redmine_user +msgid "This is the user who will be set to tasks and account analytic lines if the Redmine user does not match any Odoo user, when importing issues from Redmine." +msgstr "Das ist der Benutzer, der beim Ticket-Import aus Redmine an den Aufgaben und Zeiteinträgen gesetzt wird, wenn der eigentliche Redmine-Benutzer keinem Odoo-Benutzer entspricht." + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__odoo_id +msgid "Timesheet" +msgstr "Zeiterfassung" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__allow_timesheets +msgid "Timesheets can be logged on this task." +msgstr "Zeiterfassungen können für diese Aufgabe vorgenommen werden." + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__name +msgid "Title" +msgstr "Titel" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__total_hours_spent +msgid "Total Hours" +msgstr "Gesamtstunden" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__remaining_hours +msgid "Total remaining time, can be re-estimated periodically by the assignee of the task." +msgstr "Verbleibende Gesamtzeit, kann periodisch neu berechnet werden durch den zugewiesenen verantwortlichen Mitarbeiter." + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__product_uom_id +msgid "Unit of Measure" +msgstr "Mengeneinheit" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__message_unread +msgid "Unread Messages" +msgstr "Ungelesene Nachrichten" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__message_unread_counter +msgid "Unread Messages Counter" +msgstr "Zähler der ungelesenen Nachrichten" + +#. module: connector_redmine +#: selection:project.task,priority:0 +msgid "Urgent" +msgstr "" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_account_analytic_line__user_id +msgid "User" +msgstr "Benutzer" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__user_email +msgid "User Email" +msgstr "Benutzer E-Mail" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_backend__verify_ssl +msgid "Verify SSL" +msgstr "SSL verifizieren" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_backend__version +msgid "Version" +msgstr "" + +#. module: connector_redmine +#: selection:project.task,priority:0 +msgid "Very High" +msgstr "Sehr hoch" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__email_cc +msgid "Watchers Emails" +msgstr "E-Mails des Abonnenten" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__website_message_ids +msgid "Website Messages" +msgstr "Website-Nachrichten" + +#. module: connector_redmine +#: model:ir.model.fields,help:connector_redmine.field_redmine_issue__website_message_ids +msgid "Website communication history" +msgstr "Website-Kommunikationshistorie" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_project_task__progress +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__progress +msgid "Working Time Recorded" +msgstr "Erfasste Arbeitszeit" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__working_days_open +msgid "Working days to assign" +msgstr "Zuzuweisende Arbeitstage" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__working_days_close +msgid "Working days to close" +msgstr "Arbeitstage bis Abschluss" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__working_hours_open +msgid "Working hours to assign" +msgstr "Zuzuweisende Arbeitsstunden" + +#. module: connector_redmine +#: model:ir.model.fields,field_description:connector_redmine.field_redmine_issue__working_hours_close +msgid "Working hours to close" +msgstr "Arbeitsstunden bis Abschluss" + +#. module: connector_redmine +#: model_terms:ir.ui.view,arch_db:connector_redmine.view_redmine_backend_form +msgid "e.g. http://proxy:8081" +msgstr "z.B. http://proxy:8081" + +#. module: connector_redmine +#: model_terms:ir.ui.view,arch_db:connector_redmine.view_redmine_backend_form +msgid "e.g. http://redmine.url/" +msgstr "z.B. http://redmine.url/" + +#. module: connector_redmine +#: model_terms:ir.ui.view,arch_db:connector_redmine.edit_project_redmine +msgid "e.g. http://redmine.url/projects/projectname/" +msgstr "z.B. http://redmine.url/projects/projectname/" + diff --git a/connector_redmine/models/__init__.py b/connector_redmine/models/__init__.py new file mode 100644 index 0000000..6988abe --- /dev/null +++ b/connector_redmine/models/__init__.py @@ -0,0 +1,8 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import redmine_backend +from . import redmine_binding + +from . import redmine_issue +from . import analytic_account +from . import project diff --git a/connector_redmine/models/analytic_account.py b/connector_redmine/models/analytic_account.py new file mode 100644 index 0000000..a035c64 --- /dev/null +++ b/connector_redmine/models/analytic_account.py @@ -0,0 +1,15 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class AccountAnalyticLine(models.Model): + _inherit = "account.analytic.line" + + redmine_time_entry_id = fields.Integer(string="Redmine ID") + + last_update = fields.Datetime( + string="Last updated on", + help="During last issue import this was the date and time " + "this time entry was last updated in Redmine.", + ) diff --git a/connector_redmine/models/project.py b/connector_redmine/models/project.py new file mode 100644 index 0000000..c90c475 --- /dev/null +++ b/connector_redmine/models/project.py @@ -0,0 +1,159 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import logging +from datetime import datetime + +from odoo import api, fields, models +from odoo.exceptions import ValidationError +from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT +from odoo.tools.translate import _ + +_logger = logging.getLogger(__name__) + + +class Project(models.Model): + _inherit = "project.project" + + default_redmine_user = fields.Many2one( + string="Default Redmine User", + comodel_name="res.users", + help="This is the user who will be set to tasks and account " + "analytic lines if the Redmine user does not match any " + "Odoo user, when importing issues from Redmine.", + ) + + last_imported_on = fields.Datetime( + string="Last imported on", + help="During last issue import this was the date " + "and time of the last updated issue.", + ) + + redmine_project_url = fields.Char(string="Redmine Project") + + @api.multi + def import_issues_from_redmine(self): + """ + Call the connector to import Redmine issues for a project. + If mapping errors occur, post a comment below project. + """ + self.check_access_rule("write") + + backend = self.get_project_backend() + + last_imported_on = None + if self.last_imported_on: + lst_on = datetime.strftime( + self.last_imported_on, DEFAULT_SERVER_DATETIME_FORMAT + ) # port to v12 + last_imported_on = datetime.strptime(lst_on, DEFAULT_SERVER_DATETIME_FORMAT) + + with backend.work_on("redmine.issue") as work: + importer = work.component(usage="batch.importer") + mapping_errors = importer.run_single_project( + project_name=self.name, + project_url=self.redmine_project_url, + last_imported_on=last_imported_on, + ) + + # TODO: Collect all mapping errors and post them on project + if mapping_errors: + part_1 = _("Some issues were not imported from Redmine.") + part_2 = "\n".join({e.message for e in mapping_errors}) + body = "%s\n%s" % (part_1, part_2) + + # Log the message using SUPERUSER_ID, because otherwise + # the user will not be notified by email and might see + # that some entries were not imported. + self.sudo().message_post( + body=body, + type="comment", + subtype="mail.mt_comment", + content_subtype="plaintext", + ) + + def get_project_backend(self): + """ + Redmine project URLs look like this: http://redmine.url/projects/projectname + The Redmine backend location is this URL without further paths: http://redmine.url/ + :return: redmine backend object + """ + url = self.redmine_project_url.split("projects")[0] + + backend = ( + self.env["redmine.backend"] + .sudo() + .search(["|", ("location", "=", url), ("location", "=", url[:-1])]) + ) + + if not backend: + raise ValidationError( + _("No Redmine backend configured that has this location: %s.") % url + ) + + return backend + + @api.model + def _run_import_issues(self): + """ This method is called from a cron job. """ + projects = self.search([("redmine_project_url", "!=", False)]) + if projects: + for project in projects: + project.import_issues_from_redmine() + + +class ProjectTask(models.Model): + _inherit = "project.task" + + last_update = fields.Datetime( + string="Last updated on", + help="During last issue import this was the date and " + "time this issue was last updated in Redmine.", + ) + + # planned hours is either directly imported from Redmine + # or computed in finalize step of mapping + planned_hours = fields.Float(string="Initially Planned Hours") + + # progress is imported from Redmine, not computed anymore + progress = fields.Float(string="Working Time Recorded", compute=False) + + redmine_issue_url = fields.Char(string="Redmine Issue", readonly=True) + + redmine_user = fields.Char( + string="Redmine User", + help="This is the name of the assigned Redmine User. " + "It is set, if the Redmine user is no Odoo user.", + ) + + user_id = fields.Many2one(track_visibility=False) + priority = fields.Selection(selection_add=[("4", "Urgent"), ("5", "Immediately")]) + + @api.constrains("parent_id", "child_ids") + def _check_subtask_level(self): + """ + Since Odoo 12 it is not allowed to have multi-level task hierarchy. + We want it though. So, no contrain for parent_id and child_ids needed. + """ + pass + + @api.depends("child_ids") + def _compute_subtask_count(self): + """ + Since Odoo 12 it is not allowed to have multi-level task hierarchy. + We want it though. So, the read_group-method cannot be used here. + The calculation method from Odoo 11 is used again. + """ + for task in self: + task.subtask_count = self.search_count( + [("id", "child_of", task.id), ("id", "!=", task.id)] + ) + + +class ProjectTaskType(models.Model): + _inherit = "project.task.type" + + redmine_issue_state = fields.Char( + string="Redmine Issue State", + help="Please enter which Redmine Issue-State shall " + "be mapped to this task type.", + ) diff --git a/connector_redmine/models/redmine_backend.py b/connector_redmine/models/redmine_backend.py new file mode 100644 index 0000000..a23bd3f --- /dev/null +++ b/connector_redmine/models/redmine_backend.py @@ -0,0 +1,110 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import api, fields, models +from odoo.exceptions import UserError, Warning +from odoo.tools.misc import ustr +from odoo.tools.translate import _ + + +class RedmineBackend(models.Model): + _name = "redmine.backend" + _description = "Redmine Backend" + _inherit = "connector.backend" + _rec_name = "location" + + def _select_versions(self): + return [("1.3", _("1.3 and higher"))] + + location = fields.Char( + "Location", + size=128, + required=True, + ) + key = fields.Char( + "Key", + size=64, + required=True, + groups="connector.group_connector_manager", + ) + version = fields.Selection(_select_versions, string="Version", required=True) + proxy = fields.Char( + "Proxy", + size=128, + required=False, + ) + verify_ssl = fields.Boolean( + "Verify SSL", + default=True, + ) + + name = fields.Char("Name") + + contract_ref = fields.Char( + string="Redmine Project Identification", + help="This is the name of the field added in Redmine used to relate " + "a Redmine project to an Odoo project.\n" + "Each Redmine project must have an unique value for this attribute.", + ) + + @api.multi + def check_auth(self): + """ + Check the authentication with Redmine + """ + self.ensure_one() + with self.work_on("redmine.backend") as work: + adapter = work.component(usage="backend.adapter") + + try: + adapter._auth() + except Exception: + raise Warning(_("Could not connect to Redmine.")) + + raise Warning( + _("Connection test succeeded. " "Everything seems properly set up.") + ) + + @api.multi + def check_contract_ref(self): + """ + Check if the contract_ref field exists in Redmine. + """ + self.ensure_one() + with self.work_on("redmine.backend") as work: + adapter = work.component(usage="backend.adapter") + try: + adapter._auth() + except Exception as e: + raise UserError(_("Connection to Redmine failed: %s") % ustr(e)) + + projects = adapter.redmine_api.project.all() + exist = False + assigned = False + + if projects: + if hasattr(projects[0], "custom_fields"): + for cs in projects[0].custom_fields: + if cs["name"] == self.contract_ref: + exist = True + assigned = bool(cs["value"]) + elif hasattr(projects[0], self.contract_ref): + exist = True + + if not exist: + raise UserError( + _( + "Redmine backend configuration error:\n" + "The Redmine project identification name doesn't exist." + ) + ) + elif not assigned: + raise UserError( + _( + "Redmine backend configuration error:\n" + "The Redmine project identification field is not assigned in Redmine." + ) + ) + else: + raise UserError( + _("Connection test succeeded. Everything seems properly set up.") + ) diff --git a/connector_redmine/models/redmine_binding.py b/connector_redmine/models/redmine_binding.py new file mode 100644 index 0000000..57c2de2 --- /dev/null +++ b/connector_redmine/models/redmine_binding.py @@ -0,0 +1,16 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class RedmineBinding(models.AbstractModel): + _name = "redmine.binding" + _inherit = "external.binding" + _description = "Redmine Binding (Abstract)" + + backend_id = fields.Many2one( + "redmine.backend", "Redmine Backend", required=True, ondelete="restrict" + ) + redmine_id = fields.Integer("ID in Redmine", required=True) + sync_date = fields.Datetime("Last Synchronization Date", required=True) + updated_on = fields.Datetime("Last Update in Redmine") diff --git a/connector_redmine/models/redmine_issue.py b/connector_redmine/models/redmine_issue.py new file mode 100644 index 0000000..4403e3d --- /dev/null +++ b/connector_redmine/models/redmine_issue.py @@ -0,0 +1,66 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import logging + +from odoo import api, fields, models +from odoo.addons.queue_job.job import job + +_logger = logging.getLogger(__name__) + + +class RedmineIssue(models.Model): + _name = "redmine.issue" + _description = "Redmine Issue Binding" + _inherit = "redmine.binding" + _inherits = {"project.task": "odoo_id"} + + odoo_id = fields.Many2one( + string="Task", comodel_name="project.task", required=True, ondelete="cascade" + ) + + timesheet_ids = fields.One2many( + string="Redmine Time Entries", + comodel_name="redmine.account.analytic.line", + inverse_name="issue_id", + ) + + @job + @api.model + def import_record(self, backend, record_id): + with backend.work_on(self._name) as work: + importer = work.component(usage="record.importer") + return importer.run(record_id) + + +class RedmineTimeEntry(models.Model): + _name = "redmine.account.analytic.line" + _description = "Redmine Time Entry Binding" + _inherit = "redmine.binding" + _inherits = {"account.analytic.line": "odoo_id"} + + odoo_id = fields.Many2one( + string="Timesheet", + comodel_name="account.analytic.line", + required=True, + ondelete="cascade", + ) + + issue_id = fields.Many2one( + string="Redmine Issue", + comodel_name="redmine.issue", + required=True, + ondelete="cascade", + index=True, + ) + + @api.model + def create(self, values): + """ + Create a time entry binding with reference to odoo task + if task did not existed before import. If the task already + existed, it is set during time entry mapping. + """ + if not values.get("task_id", False): + redmine_issue = self.env["redmine.issue"].browse(values["issue_id"]) + values["task_id"] = redmine_issue.odoo_id.id + return super(RedmineTimeEntry, self).create(values) diff --git a/connector_redmine/security/ir.model.access.csv b/connector_redmine/security/ir.model.access.csv new file mode 100644 index 0000000..4ffff73 --- /dev/null +++ b/connector_redmine/security/ir.model.access.csv @@ -0,0 +1,7 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_redmine_manager,access_redmine_manager,model_redmine_backend,connector.group_connector_manager,1,1,1,1 +access_redmine_user,access_redmine_user,model_redmine_backend,base.group_user,1,0,0,0 +access_redmine_issue_user,access_redmine_issue,model_redmine_issue,project.group_project_user,1,0,0,0 +access_redmine_issue_line_manager,access_redmine_issue,model_redmine_issue,project.group_project_manager,1,1,1,1 +access_redmine_account_analytic_line_user,access_redmine_account_analytic_line,model_redmine_account_analytic_line,project.group_project_user,1,0,0,0 +access_redmine_account_analytic_line_manager,access_redmine_account_analytic_line,model_redmine_account_analytic_line,project.group_project_manager,1,1,1,1 diff --git a/connector_redmine/static/src/img/icon.png b/connector_redmine/static/src/img/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..89e1f78532bc4417f9dd9fe74ea9db338e6a54ea GIT binary patch literal 10600 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 + + + + + + + + + + + + + + + + + + + + + + + diff --git a/connector_redmine/tests/__init__.py b/connector_redmine/tests/__init__.py new file mode 100644 index 0000000..6b9e58c --- /dev/null +++ b/connector_redmine/tests/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import test_connector_redmine +from . import test_import_issues diff --git a/connector_redmine/tests/common.py b/connector_redmine/tests/common.py new file mode 100644 index 0000000..c99c542 --- /dev/null +++ b/connector_redmine/tests/common.py @@ -0,0 +1,136 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from collections import namedtuple +from datetime import datetime + +from odoo.addons.component.tests.common import TransactionComponentCase + +Issue = namedtuple("Issue", "id subject") + +Activity = namedtuple("Activity", "id name") + +TimeEntry = namedtuple( + "TimeEntry", "issue_id issue spent_on hours activity activity_id comments" +) + + +class BaseRedmineTestCase(TransactionComponentCase): + def setUp(self): + super(BaseRedmineTestCase, self).setUp() + self.backend_model = self.env["redmine.backend"] + self.user_model = self.env["res.users"] + self.employee_model = self.env["hr.employee"] + self.timesheet_model = self.env["account.analytic.line"] + self.account_model = self.env["account.analytic.account"] + self.general_account_model = self.env["account.account"] + self.product_model = self.env["product.product"] + self.redmine_model = self.env["redmine.account.analytic.line"] + self.project_model = self.env["project.project"] + self.task_model = self.env["project.task"] + self.issue_model = self.env["redmine.issue"] + + self.user_1 = self.user_model.create( + { + "name": "User 1", + "login": "user_1", + } + ) + + self.project = self.project_model.create( + { + "name": "Project_1", + "allow_timesheets": True, + "privacy_visibility": "employees", + "redmine_project_url": "http://localhost:3000/projects/project1", + } + ) + self.task = self.task_model.create( + { + "name": "Task One", + "priority": "0", + "kanban_state": "normal", + "project_id": self.project.id, + } + ) + self.account = self.project.analytic_account_id + + self.project_2 = self.project_model.create( + { + "name": "Project_2", + "allow_timesheets": True, + "privacy_visibility": "employees", + "redmine_project_url": "http://localhost:3000/projects/project2", + } + ) + + self.account_2 = self.project_2.analytic_account_id + + self.product = self.product_model.search([("type", "=", "service")])[0] + + self.general_account = self.general_account_model.create( + { + # 'type': 'other', + "code": "123123", + "name": "test", + # 'user_type': self.ref('account.data_account_type_expense'), + "user_type_id": self.ref("account.data_account_type_expenses"), + } + ) + + self.product.write( + { + "property_account_expense_id": self.general_account.id, + } + ) + + self.employee = self.employee_model.create( + { + "name": "Employee 1", + "user_id": self.user_1.id, + # 'journal_id': journal.id, + # 'product_id': self.product.id, + } + ) + + self.backend = self.backend_model.create( + { + "name": "redmine_test", + "location": self.project.redmine_project_url, + "key": "39730056c1df6fb97b4fa5b9eb4bd37221ca1223", + "version": "1.3", + "contract_ref": "abcd", + # "location": "http://localhost:3000/", + } + ) + + + self.now = datetime.now() + self.date_now = datetime.now().date() + + self.issue = self.issue_model.create( + { + "name": "Redmine issue", + "odoo_id": self.task.id, + "backend_id": self.backend.id, + "redmine_id": 123, + "sync_date": self.now, + } + ) + + self.timesheet_vals = { + "redmine_id": 123, + "updated_on": self.now, + "backend_id": self.backend.id, + "sync_date": self.now, + "user_id": self.user_1.id, + "date": self.date_now, + "account_id": self.account.id, + # 'account_id': self.project.analytic_account_id.id, + # 'task_id': , + "issue_id": self.issue.id, + "employee_id": self.employee.id, + "unit_amount": 5, + "name": "Test", + } + + self.redmine_timesheet = self.redmine_model.create(self.timesheet_vals) diff --git a/connector_redmine/tests/test_connector_redmine.py b/connector_redmine/tests/test_connector_redmine.py new file mode 100644 index 0000000..550c174 --- /dev/null +++ b/connector_redmine/tests/test_connector_redmine.py @@ -0,0 +1,118 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from datetime import datetime + +from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT + +from .common import BaseRedmineTestCase + + +class TestRedmineConnector(BaseRedmineTestCase): + def setUp(self): + super(TestRedmineConnector, self).setUp() + + def test_01_redmine_mapper(self): + with self.backend.work_on("redmine.account.analytic.line") as work: + mapper_obj = work.component(usage="import.mapper") + + update = datetime(2015, 1, 1, 8, 32, 10) + record = {"updated_on": update} + + self.assertEqual( + mapper_obj.updated_on(record), + {"updated_on": update.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, + ) + + self.assertEqual( + mapper_obj.backend_id(record), {"backend_id": self.backend.id} + ) + + now = datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT) + self.assertEqual(mapper_obj.sync_date(record), {"sync_date": now}) + + def test_02_redmine_binder_to_internal(self): + with self.backend.work_on("redmine.account.analytic.line") as work: + binder_obj = work.component(usage="binder") + + # Without the unwrap parameter, to_internal must return + # the binding (redmine.account.analytic.line) + binding = binder_obj.to_internal(123) + + self.assertEqual(binding.date, self.date_now) + self.assertEqual(binding.redmine_id, 123) + + # With the unwrap parameter, to_internal must return + # the ID of the odoo model record (account.analytic.line) + timesheet = binder_obj.to_internal(123, unwrap=True) + self.assertEqual(timesheet.date, self.date_now) + + def test_03_redmine_binder_to_external(self): + with self.backend.work_on("redmine.account.analytic.line") as work: + binder_obj = work.component(usage="binder") + binding = self.redmine_timesheet + timesheet = binding.odoo_id + # Without the unwrap parameter, to_external must return + # the external_id + redmine_id = binder_obj.to_external(timesheet, wrap=True) + redmine_id_2 = binder_obj.to_external(binding) + + self.assertEqual(redmine_id, redmine_id_2) + self.assertEqual(redmine_id, 123) + + def test_04_redmine_binder_unwrap_binding(self): + """If the object is already a binding then unwrap_binding should return + it. Otherwise it should browse it.""" + + with self.backend.work_on("redmine.account.analytic.line") as work: + binder_obj = work.component(usage="binder") + binding = self.redmine_timesheet + + timesheet = binder_obj.unwrap_binding(binding) + self.assertEqual(timesheet.date, self.date_now) + + timesheet_2 = binder_obj.unwrap_binding(binding.id) + self.assertEqual(timesheet, timesheet_2) + + def test_05_redmine_binder_unwrap_model(self): + with self.backend.work_on("redmine.account.analytic.line") as work: + binder_obj = work.component(usage="binder") + self.assertEqual(binder_obj.unwrap_model(), "account.analytic.line") + + # TODO: Enable when importing time entry + + def test_06_import_synchronizer_get_binding_id(self): + with self.backend.work_on("redmine.account.analytic.line") as work: + synchronizer = work.component(usage="record.importer") + + binding = self.redmine_model.search([("redmine_id", "=", 123)])[0] + + synchronizer.redmine_id = 123 + self.assertEqual(synchronizer._get_binding(), binding) + + def test_07_import_synchronizer_update(self): + with self.backend.work_on("redmine.account.analytic.line") as work: + synchronizer = work.component(usage="record.importer") + + synchronizer.redmine_id = 123 + + new_vals = { + "name": "New Name", + "unit_amount": 10, + } + synchronizer._update(synchronizer._get_binding(), new_vals) + + for val in new_vals: + self.assertEqual(new_vals[val], self.redmine_timesheet[val]) + + def test_08_import_synchronizer_create(self): + with self.backend.work_on("redmine.account.analytic.line") as work: + synchronizer = work.component(usage="record.importer") + synchronizer.redmine_id = 345 + + vals = self.timesheet_vals + vals["redmine_id"] = 345 + vals["name"] = "Second Timesheet" + + binding = synchronizer._create(vals) + + self.assertEqual(binding.name, vals["name"]) diff --git a/connector_redmine/tests/test_import_issues.py b/connector_redmine/tests/test_import_issues.py new file mode 100644 index 0000000..3e00fe6 --- /dev/null +++ b/connector_redmine/tests/test_import_issues.py @@ -0,0 +1,177 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from datetime import datetime + +from .common import Activity, BaseRedmineTestCase, Issue, TimeEntry + +# from mock import patch +# from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT + + +import_job_path = ( + "odoo.addons.connector_redmine.unit.import_synchronizer." "import_record.delay" +) + + +def mock_delay(model_name, *args, **kwargs): + """Enqueue the function. Return the uuid of the created job.""" + return import_record(model_name, *args, **kwargs) + + +class TestImportIssues(BaseRedmineTestCase): + def setUp(self): + super(TestImportIssues, self).setUp() + self.project_model = self.env["project.project"] + self.task_model = self.env["project.task"] + self.issue_model = self.env["redmine.issue"] + + def get_time_entry_defaults(self): + issue = Issue( + id=self.issue.redmine_id, + subject=self.issue.name, + ) + activity = Activity( + id=100, + name="test Activity 1", + ) + time_entry = TimeEntry( + issue_id=issue.id, + issue=issue, + activity_id=activity.id, + activity=activity, + hours=8.5, + spent_on="2015-01-01", + comments="Test Mapping of Time Entry", + ) + res = { + "contract_ref": self.project.name, + "updated_on": datetime(2015, 4, 8, 20, 38, 31), + "time_entry": time_entry, + "user_login": "user_1", + "redmine_id": 123, + } + return res + + def test_01_mapper_analytic_account(self): + """ + Test that the proper analytic account is mapped + """ + with self.backend.work_on("redmine.account.analytic.line") as work: + mapper_obj = work.component(usage="import.mapper") + + defaults = self.get_time_entry_defaults() + map_record = mapper_obj.map_record(defaults) + res = map_record.values(for_create=True) + + self.assertEqual(res["account_id"], self.account.id) + + defaults["contract_ref"] = "Project_2" + map_record = mapper_obj.map_record(defaults) + res = map_record.values() + + self.assertEqual(res["account_id"], self.account_2.id) + + def test_02_binder(self): + with self.backend.work_on("redmine.account.analytic.line") as work: + binder = work.component(usage="binder") + mapper_obj = work.component(usage="import.mapper") + + defaults = self.get_time_entry_defaults() + map_record = mapper_obj.map_record(defaults) + data = map_record.values(for_create=True) + binding_id = self.env["redmine.account.analytic.line"].create(data).id + + binder.bind(123, binding_id) + + # TODO: Implement the following tests according to the last changes + # in the port + + # @patch(import_job_path, side_effect=mock_delay) + # def import_time_entry_batch( + # self, record_id, defaults, job_decorator, options=None + # ): + # adapter = backend_adapter.TimeEntryAdapter + # with patch.object(adapter, 'read') as read, \ + # patch.object(adapter, 'search') as search: + + # search.return_value = [record_id] + # read.return_value = defaults + + # import_batch( + # 'redmine.account.analytic.line', + # self.backend, filters={ + # 'from_date': '2015-01-01', + # 'to_date': '2015-01-07', + # }, options=options) + + # def test_03_import_batch_synchronizer(self): + # defaults = self.get_time_entry_defaults() + # self.import_time_entry_batch(123, defaults) + + # timesheet = self.redmine_model.search([('redmine_id', '=', 123)])[0] + + # self.assertEqual(timesheet.account_id, self.account) + # self.assertEqual(timesheet.unit_amount, 8.5) + # self.assertEqual(timesheet.date, '2015-01-01') + # self.assertEqual(timesheet.user_id, self.user_1) + # self.assertEqual(timesheet.product_id.id, self.product.id) + # self.assertEqual(timesheet.amount, -8.5 * 30) + + # defaults['contract_ref'] = 'efgh' + # defaults['hours'] = 10 + # defaults['spent_on'] = '2015-01-02' + # self.import_time_entry_batch(123, defaults) + + # self.backend.refresh() + # self.assertEqual( + # self.backend.time_entry_last_update, + # datetime(2015, 4, 8, 20, 38, 31).strftime( + # DEFAULT_SERVER_DATETIME_FORMAT)) + + # timesheet.refresh() + # self.assertEqual(timesheet.account_id, self.account_2) + # self.assertEqual(timesheet.unit_amount, 10) + # self.assertEqual(timesheet.date, '2015-01-02') + + # def test_04_cron_job(self): + # """ + # Test that the import cron job executes without error + # """ + # self.backend_model.prepare_time_entry_import() + + # def test_05_import_single_user_time_entries_mapping_error(self): + # with self.backend.work_on('redmine.account.analytic.line') as work: + # adapter = work.component(usage='backend.adapter') + # with patch.object(adapter, 'search_user') as search_user, \ + # patch.object(adapter, 'read') as read, \ + # patch.object(adapter, 'search') as search: + + # defaults = self.get_time_entry_defaults() + + # updated_on = self.project.last_imported_on + + # search_user.return_value = 1 + # search.return_value = [1, 2] + + # def side_effect(redmine_id): + # if redmine_id == 1: + # return defaults + # return dict(defaults, contract_ref='not mapped') + + # read.side_effect = side_effect + + # self.project.import_issues_from_redmine() + + # # Time entry 1 is mapped, but entry 2 is not mapped. + # # So one is created and the other is logged in the chatter. + # self.assertEqual(len(timesheet.timesheet_ids), 1) + + # entry = timesheet.timesheet_ids[0] + # self.assertEqual(entry.to_invoice.id, self.ref( + # 'hr_timesheet_invoice.timesheet_invoice_factor2')) + + # self.backend.refresh() + + # # Backend field time_entry_last_update must not be updated when + # # querying timesheets for a single user + # self.assertEqual(updated_on, self.project.last_imported_on) diff --git a/connector_redmine/views/hr_timesheet_view.xml b/connector_redmine/views/hr_timesheet_view.xml new file mode 100644 index 0000000..0c5e087 --- /dev/null +++ b/connector_redmine/views/hr_timesheet_view.xml @@ -0,0 +1,15 @@ + + + + + account.analytic.line.tree.hr_timesheet + account.analytic.line + + + + + + + + + \ No newline at end of file diff --git a/connector_redmine/views/project_views.xml b/connector_redmine/views/project_views.xml new file mode 100644 index 0000000..076b421 --- /dev/null +++ b/connector_redmine/views/project_views.xml @@ -0,0 +1,87 @@ + + + + + project.project.form + project.project + + + +
+ + + +
+
+ + + + + + +
+
+ + + + project.task.form.inherit2 + project.task + + + + + + + + + + + + + + + + + + + + + project.task.kanban.redmine + project.task + + + + + + + + + + project.task.tree.redmine + project.task + + + + + + + + + + + project.task.type.form.inherit + project.task.type + + + + + + + +
diff --git a/connector_redmine/views/redmine_backend_view.xml b/connector_redmine/views/redmine_backend_view.xml new file mode 100644 index 0000000..1a538f2 --- /dev/null +++ b/connector_redmine/views/redmine_backend_view.xml @@ -0,0 +1,76 @@ + + + + + + redmine.backend.form + redmine.backend + +
+ +