diff --git a/odoo_jira_connector/README.rst b/odoo_jira_connector/README.rst new file mode 100755 index 000000000..128f24cdc --- /dev/null +++ b/odoo_jira_connector/README.rst @@ -0,0 +1,47 @@ +.. image:: https://img.shields.io/badge/licence-LGPL--3-green.svg + :target: https://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 + +Odoo Jira Connector +=================== +* Connect Odoo to Jira + +Configuration: +-------------- +After installing this module, you need to set the URL for Jira, user name and API token. +Test the connection. Once the connection is established, you can export/ import the projects, tasks, and users. + +Company +------- +* `Cybrosys Techno Solutions `__ + +License +------- +General Public License, Version 3 (LGPL v3). +(https://www.gnu.org/licenses/lgpl-3.0-standalone.html) + +Credits +------- +Developer: (V15) Rosmy John, Contact: odoo@cybrosys.com + +Contacts +-------- +* Mail Contact : odoo@cybrosys.com +* Website : https://cybrosys.com + +Bug Tracker +----------- +Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. + +Maintainer +========== +.. image:: https://cybrosys.com/images/logo.png + :target: https://cybrosys.com + +This module is maintained by Cybrosys Technologies. + +For support and more information, please visit `Our Website `__ + +Further information +=================== +HTML Description: ``__ diff --git a/odoo_jira_connector/__init__.py b/odoo_jira_connector/__init__.py new file mode 100755 index 000000000..66d0a86a7 --- /dev/null +++ b/odoo_jira_connector/__init__.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +################################################################################ +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies() +# Author: Rosmy John (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +################################################################################ +from . import models +from . import controllers + +from odoo.exceptions import UserError +from odoo import api, SUPERUSER_ID + + +def pre_init_hook(cr): + env = api.Environment(cr, SUPERUSER_ID, {}) + queue_job = env['ir.model.data'].search([('module', '=', 'queue_job')]) + queue_job_cron_jobrunner = env['ir.model.data'].search( + [('module', '=', 'queue_job_cron_jobrunner')]) + if not queue_job_cron_jobrunner or not queue_job: + raise UserError("Please make sure you have added and installed Queue " + "Job and Queue Job Cron Jobrunner in your system") diff --git a/odoo_jira_connector/__manifest__.py b/odoo_jira_connector/__manifest__.py new file mode 100755 index 000000000..318de9e69 --- /dev/null +++ b/odoo_jira_connector/__manifest__.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +############################################################################### +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies() +# Author: Rosmy John (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +############################################################################### +{ + 'name': 'Odoo Jira Connector', + 'version': '15.0.1.0.0', + 'category': 'Project', + 'summary': 'Odoo Jira Connector is a valuable integration tool for ' + 'businesses that use both Odoo and Jira. By connecting these ' + 'two systems, businesses can streamline their project ' + 'management processes and improve their overall efficiency.', + 'description': 'The Odoo Jira Connector offers a range of features, ' + 'including bi-directional synchronization of data, ' + 'automatic creation of Jira issues from Odoo records, and ' + 'real-time updates of Jira issues in Odoo. To meet the ' + 'specific needs of any business users can leverage, they ' + 'can use Odoo to handle their business.', + 'author': 'Cybrosys Techno Solutions', + 'company': 'Cybrosys Techno Solutions', + 'maintainer': 'Cybrosys Techno Solutions', + 'website': 'https://www.cybrosys.com', + 'depends': ['project'], + 'data': [ + 'security/ir.model.access.csv', + 'views/res_config_settings_views.xml', + 'views/res_users_views.xml', + 'views/project_views.xml', + 'views/project_task_type_views.xml', + 'views/jira_sprint_views.xml', + ], + 'images': ['static/description/banner.png'], + 'license': 'LGPL-3', + 'installable': True, + 'application': False, + 'auto_install': False, + 'pre_init_hook': 'pre_init_hook' +} diff --git a/odoo_jira_connector/controllers/__init__.py b/odoo_jira_connector/controllers/__init__.py new file mode 100644 index 000000000..016348fa2 --- /dev/null +++ b/odoo_jira_connector/controllers/__init__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +################################################################################ +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies() +# Author: Rosmy John (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +################################################################################ +from . import odoo_jira_connector diff --git a/odoo_jira_connector/controllers/odoo_jira_connector.py b/odoo_jira_connector/controllers/odoo_jira_connector.py new file mode 100644 index 000000000..bb129742a --- /dev/null +++ b/odoo_jira_connector/controllers/odoo_jira_connector.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +################################################################################ +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies() +# Author: Rosmy John (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +################################################################################ +import json +from odoo import http +from odoo.http import request + + +class JiraWebhook(http.Controller): + """Class to fetch Jira data using webhook""" + + @http.route('/jira_webhook', type="json", auth='public', + methods=['POST'], csrf=False) + def import_jira_data(self, *args, **kwargs): + """function to import data from Jira based on webhook events""" + automated_import_export = request.env['ir.config_parameter'] \ + .sudo().get_param('odoo_jira_connector.automatic') + if automated_import_export: + data = json.loads(request.httprequest.data) + jira = json.dumps(data, sort_keys=True, + indent=4, separators=(',', ': ')) + jira_data = json.loads(jira) + webhook_event = jira_data['webhookEvent'] + delay = request.env['project.task'].sudo(). \ + with_delay(priority=1, eta=60) + delay.webhook_data_handle(jira_data, webhook_event) diff --git a/odoo_jira_connector/doc/RELEASE_NOTES.md b/odoo_jira_connector/doc/RELEASE_NOTES.md new file mode 100755 index 000000000..5d4a93212 --- /dev/null +++ b/odoo_jira_connector/doc/RELEASE_NOTES.md @@ -0,0 +1,6 @@ +## Module + +#### 01.08.2024 +#### Version 15.0.1.0.0 +#### ADD +- Initial commit for Odoo Jira Connector diff --git a/odoo_jira_connector/models/__init__.py b/odoo_jira_connector/models/__init__.py new file mode 100755 index 000000000..d4aa68d3b --- /dev/null +++ b/odoo_jira_connector/models/__init__.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +################################################################################ +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies() +# Author: Rosmy John (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +################################################################################ +from . import ir_attachment +from . import jira_sprint +from . import mail_message +from . import project +from . import project_task_type +from . import res_config_settings +from . import res_users diff --git a/odoo_jira_connector/models/ir_attachment.py b/odoo_jira_connector/models/ir_attachment.py new file mode 100644 index 000000000..eed8c1603 --- /dev/null +++ b/odoo_jira_connector/models/ir_attachment.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +################################################################################ +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies() +# Author: Rosmy John (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +################################################################################ +import base64 +import os +import requests +from odoo import models, fields, api +# The Header parameters +HEADERS = {'Accept': 'application/json', 'Content-Type': 'application/json'} + + +class IrAttachment(models.Model): + _inherit = 'ir.attachment' + + attachment_id_jira = fields.Integer(string="Jira ID", + help="Jira id of attachment.") + + @api.model_create_multi + def create(self, values_list): + """ For creating attachment in Jira and attachment in the chatter """ + attachment = super(IrAttachment, self).create(values_list) + if not values_list[0].get('attachment_id_jira'): + ir_config_parameter = self.env['ir.config_parameter'].sudo() + if ir_config_parameter.get_param('odoo_jira_connector.connection'): + url = ir_config_parameter.get_param('odoo_jira_connector.url') + user = ir_config_parameter.get_param( + 'odoo_jira_connector.user_id_jira') + password = ir_config_parameter.get_param( + 'odoo_jira_connector.api_token') + if attachment.res_model == 'project.task': + task = self.env['project.task'].browse(attachment.res_id) + attachment_url = url + 'rest/api/3/issue/%s/' \ + 'attachments' % task.task_id_jira + attachment_type = (self.env['res.config.settings']. + find_attachment_type(attachment)) + if attachment.datas and attachment_type in ( + 'pdf', 'xlsx', 'jpg'): + temp_file_path = f'/tmp/temp.{attachment_type}' + binary_data = base64.b64decode(attachment.datas) + # Save the binary data to a file + with open(temp_file_path, 'wb') as file: + file.write(binary_data) + if attachment_type == 'jpg' and os.path.splitext( + temp_file_path)[1].lower() != '.jpg': + # Rename the saved file to its corresponding JPG + # file format + file_path = os.path.splitext(temp_file_path)[ + 0] + '.jpg' + os.rename(temp_file_path, file_path) + temp_file_path = file_path + attachment_file = { + 'file': ( + attachment.name, open(temp_file_path, 'rb')) + } + response = requests.post(attachment_url, + headers={ + 'X-Atlassian-Token': + 'no-check'}, + files=attachment_file, + auth=(user, password)) + data = response.json() + attachment.write( + {'attachment_id_jira': data[0].get('id')}) + return attachment + + def unlink(self): + """ Overrides the unlink method of attachment to delete an attachment + in Jira when we delete the attachment in Odoo""" + for attachment in self: + jira_connection = self.env['ir.config_parameter'].sudo().get_param( + 'odoo_jira_connector.connection') + if jira_connection: + jira_url = self.env['ir.config_parameter'].sudo().get_param( + 'odoo_jira_connector.url', '') + user = self.env['ir.config_parameter'].sudo().get_param( + 'odoo_jira_connector.user_id_jira') + password = self.env['ir.config_parameter'].sudo().get_param( + 'odoo_jira_connector.api_token') + if attachment.attachment_id_jira: + requests.delete( + jira_url + '/rest/api/3/attachment/' + + str(attachment.attachment_id_jira), + headers=HEADERS, auth=(user, password)) + return super(IrAttachment, self).unlink() diff --git a/odoo_jira_connector/models/jira_sprint.py b/odoo_jira_connector/models/jira_sprint.py new file mode 100644 index 000000000..288aa372b --- /dev/null +++ b/odoo_jira_connector/models/jira_sprint.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +################################################################################ +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies() +# Author: Rosmy John (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +################################################################################ +from odoo import fields, models + + +class JiraSprint(models.Model): + """class for Sprint""" + _name = "jira.sprint" + _description = "jira sprint" + + sprint_id_jira = fields.Integer(string="Sprint id", readonly=True, + help="sprint id in jira.") + name = fields.Char(string="Sprint Name", help="Name of the sprint.") + sprint_goal = fields.Text(string="Goal", help="Goal of the sprint.") + start_date = fields.Datetime(string="Start Date", help="Sprint start date.") + end_date = fields.Datetime(string="End Date", help="Sprint end date.") + project_id = fields.Many2one('project.project', readonly=True, + help="Respective Project ID.") + state = fields.Selection(string="State", + selection=[('to_start', 'To start'), + ('ongoing', 'Ongoing'), + ('completed', 'Completed')], + default='to_start', help="State of the sprint.") + + def action_get_tasks(self): + """Sprint added tasks""" + return { + 'type': 'ir.actions.act_window', + 'name': 'Tasks', + 'view_mode': 'kanban', + 'res_model': 'project.task', + 'views': [[False, 'kanban'], [False, 'tree'], [False, 'form']], + 'domain': [('project_id', '=', self.project_id.id), + ('sprint_id.state', '=', 'ongoing')], + 'context': "{'create': False}" + } + + def action_get_backlogs(self): + """Tasks in backlogs""" + return { + 'type': 'ir.actions.act_window', + 'name': 'Backlogs', + 'view_mode': 'kanban', + 'res_model': 'project.task', + 'views': [[False, 'kanban'], [False, 'tree'], [False, 'form']], + 'domain': [('project_id', '=', self.project_id.id), + ('sprint_id.state', '=', 'to_start')], + 'context': "{'create': False}" + } + + def action_get_all_tasks(self): + """All tasks in the project""" + return { + 'type': 'ir.actions.act_window', + 'name': 'All Tasks', + 'view_mode': 'kanban', + 'res_model': 'project.task', + 'views': [[False, 'kanban'], [False, 'tree'], [False, 'form']], + 'domain': [('project_id', '=', self.project_id.id)], + 'context': "{'create': False}" + } diff --git a/odoo_jira_connector/models/mail_message.py b/odoo_jira_connector/models/mail_message.py new file mode 100755 index 000000000..7021344c1 --- /dev/null +++ b/odoo_jira_connector/models/mail_message.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +############################################################################### +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies() +# Author: Rosmy John (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +############################################################################### +import json +import requests +from odoo import api, fields, models +from odoo.tools import html2plaintext + + +class MailMessage(models.Model): + """ + This class is inherited for adding an extra field and + override the create function + Methods: + create(values_list): + extends create() to create comment in Jira + """ + _inherit = 'mail.message' + + message_id_jira = fields.Integer(string='Message ID', + help='ID for the comments in Jira.') + + @api.model_create_multi + def create(self, values_list): + """ For creating comment in Jira and comments in the chatter """ + message = super(MailMessage, self).create(values_list) + if message.message_id_jira == 0: + ir_config_parameter = self.env['ir.config_parameter'].sudo() + if ir_config_parameter.get_param('odoo_jira_connector.connection'): + url = ir_config_parameter.get_param('odoo_jira_connector.url') + user = ir_config_parameter.get_param( + 'odoo_jira_connector.user_id_jira') + password = ir_config_parameter.get_param( + 'odoo_jira_connector.api_token') + if message.model == 'project.task': + task = self.env['project.task'].browse(message.res_id) + current_message = str(html2plaintext(message.body)) + response = requests.get( + f'{url}rest/api/3/issue/{task.task_id_jira}/comment', + headers={ + 'Accept': 'application/json', + 'Content-Type': 'application/json'}, + auth=(user, password)) + data = response.json() + if response.status_code == 200: + list_of_comments_jira = [ + str(comments['body']['content'][0]['content'][0][ + 'text']) for comments in data['comments']] + if current_message not in list( + filter(None, list_of_comments_jira)): + data = json.dumps({ + 'body': { + 'type': 'doc', + 'version': 1, + 'content': [{ + 'type': 'paragraph', + 'content': [{ + 'text': current_message, + 'type': 'text' + }] + }] + } + }) + response = requests.post( + url + 'rest/api/3/issue/%s/comment' % ( + task.task_id_jira), headers={ + 'Accept': 'application/json', + 'Content-Type': 'application/json'}, + data=data, auth=(user, password)) + data = response.json() + message.write({'message_id_jira': data.get('id')}) + return message diff --git a/odoo_jira_connector/models/project.py b/odoo_jira_connector/models/project.py new file mode 100755 index 000000000..4b4e878e8 --- /dev/null +++ b/odoo_jira_connector/models/project.py @@ -0,0 +1,670 @@ +# -*- coding: utf-8 -*- +################################################################################ +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies() +# Author: Rosmy John (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +################################################################################ +import base64 +import json +from datetime import datetime +import requests +from requests.auth import HTTPBasicAuth +from odoo import api, fields, models, _ +from odoo.exceptions import ValidationError +from odoo.tools import html2plaintext + +# The Header parameters +HEADERS = {'Accept': 'application/json', 'Content-Type': 'application/json'} + + +class ProjectProject(models.Model): + """ + This class is inherited for adding some extra field and override the + create and write function also to add function to show sprint + Methods: + create(vals_list): + extends create() to export project to Jira + write(vals): + extends write() to update corresponding project in Jira + """ + _inherit = 'project.project' + + project_id_jira = fields.Integer(string='Jira Project ID', + help='Corresponding project id of Jira.', + readonly=True) + jira_project_key = fields.Char(string='Jira Project Key', + help='Corresponding project key of Jira.', + readonly=True) + sprint_active = fields.Boolean(string='Sprint active', + help='To show sprint smart button.',default=True) + board_id_jira = fields.Integer(string='Jira Board ID', + help='Corresponding Board id of Jira.', + readonly=True) + + def action_get_sprint(self): + """Getting sprint inside the project""" + return { + 'type': 'ir.actions.act_window', + 'name': 'Sprints', + 'view_mode': 'tree,form', + 'res_model': 'jira.sprint', + 'context': {'default_project_id': self.id}, + 'domain': [('project_id', '=', self.id)], + } + + @api.model_create_multi + def create(self, vals_list): + """ Overrides create method of project to export project to Jira """ + self = self.with_context(mail_create_nosubscribe=True) + projects = super().create(vals_list) + jira_connection = self.env['ir.config_parameter'].sudo().get_param( + 'odoo_jira_connector.connection') + if jira_connection: + jira_url = self.env['ir.config_parameter'].sudo().get_param( + 'odoo_jira_connector.url', False) + user = self.env['ir.config_parameter'].sudo().get_param( + 'odoo_jira_connector.user_id_jira', False) + password = self.env['ir.config_parameter'].sudo().get_param( + 'odoo_jira_connector.api_token', False) + auth = HTTPBasicAuth(user, password) + project_headers = {'Accept': 'application/json'} + response = requests.request( + 'GET', jira_url + '/rest/api/3/project/', + headers=project_headers, auth=auth) + projects_json = json.dumps( + json.loads(response.text), sort_keys=True, indent=4, + separators=(',', ': ')) + project_json = json.loads(projects_json) + name_list = [project['name'] for project in project_json] + key = projects.name.upper() + project_key = key[:3] + '1' + key[-3:] + project_keys = project_key.replace(' ', '') + auth = HTTPBasicAuth(user, password) + project_payload = { + 'name': projects.name, 'key': project_keys, + 'templateKey': 'com.pyxis.greenhopper.jira:gh-simplified' + '-kanban-classic' + } + if projects.name not in name_list: + response = requests.request( + 'POST', jira_url + 'rest/simplified/latest/project', + data=json.dumps(project_payload), + headers=HEADERS, auth=auth) + data = response.json() + if 'projectId' in data: + projects.write({'project_id_jira': data['projectId'], + 'jira_project_key': data['projectKey']}) + self.env['ir.config_parameter'].sudo().set_param( + 'import_project_count', int( + self.env['ir.config_parameter'].sudo().get_param( + 'import_project_count')) + 1) + + elif 'errors' in data and 'projectName' in data['errors']: + raise ValidationError( + "A project with this name already exists. Please " + "rename the project.") + elif 'errors' in data and 'projectKey' in data['errors']: + raise ValidationError(data['errors']['projectKey']) + return projects + + def write(self, vals): + """ Overrides the write method of project.project to update project + name in Jira when we update the project in Odoo""" + jira_connection = self.env['ir.config_parameter'].sudo().get_param( + 'odoo_jira_connector.connection') + if jira_connection: + for project in self: + jira_url = self.env['ir.config_parameter'].sudo().get_param( + 'odoo_jira_connector.url') + user = self.env['ir.config_parameter'].sudo().get_param( + 'odoo_jira_connector.user_id_jira') + password = self.env['ir.config_parameter'].sudo().get_param( + 'odoo_jira_connector.api_token') + auth = (user, password) + headers = { + "Accept": "application/json", + "Content-Type": "application/json" + } + url = (f"{jira_url}/rest/api/3/project/" + f"{project.jira_project_key}") + payload = json.dumps({ + "name": vals.get('name'), + }) + payload_json = json.loads(payload) + response = requests.get( + url, + headers=headers, + auth=auth) + data = response.json() + if 'name' in data: + if data['name'] != payload_json['name']: + requests.request( + "PUT", + url, data=payload, headers=headers, auth=auth) + else: + requests.request( + "PUT", + url, data=payload, headers=headers, auth=auth) + return super(ProjectProject, self).write(vals) + + +class ProjectTask(models.Model): + """ + This class is inherited for adding some extra field and override the + create function + Methods: + create(vals_list): + extends create() to export tasks to Jira + unlink(): + extends unlink() to delete a task in Jira when we delete the + task in Odoo + write(vals): + extends write() to update a task in Jira when we update the + task in Odoo + """ + _inherit = 'project.task' + + task_id_jira = fields.Char(string='Jira Task ID', help='Task id of Jira.', + readonly=True) + sprint_id = fields.Many2one('jira.sprint', + help="Sprint of this task.", readonly=True) + task_sprint_active = fields.Boolean(string="Active Sprint", + compute="_compute_task_sprint_active", + store=True, + help="Boolean field to check whether " + "the sprint is active or not.") + + @api.depends('project_id.sprint_active') + def _compute_task_sprint_active(self): + """compute function to make sprint_id invisible by changing + 'task_sprint_active' field to true""" + for rec in self: + if rec.project_id.sprint_active: + rec.task_sprint_active = True + + @api.model + def create(self, vals_list): + """ Override the create method of tasks to export tasks to Jira """ + res = super(ProjectTask, self).create(vals_list) + jira_connection = self.env['ir.config_parameter'].sudo().get_param( + 'odoo_jira_connector.connection') + if jira_connection: + jira_url = self.env['ir.config_parameter'].sudo().get_param( + 'odoo_jira_connector.url') + user = self.env['ir.config_parameter'].sudo().get_param( + 'odoo_jira_connector.user_id_jira') + password = self.env['ir.config_parameter'].sudo().get_param( + 'odoo_jira_connector.api_token') + query = {'jql': 'project = %s' % res.project_id.jira_project_key} + requests.get(jira_url + 'rest/api/3/search', headers=HEADERS, + params=query, auth=(user, password)) + if not res.task_id_jira: + payload = json.dumps({ + 'fields': { + 'project': {'key': res.project_id.jira_project_key}, + 'summary': res.name, + 'description': html2plaintext(res.description), + 'issuetype': {'name': 'Task'} + } + }) + response = requests.post( + jira_url + '/rest/api/2/issue', headers=HEADERS, + data=payload, auth=(user, password)) + data = response.json() + res.task_id_jira = str(data.get('key')) + self.env['ir.config_parameter'].sudo().set_param( + 'export_task_count', int( + self.env['ir.config_parameter'].sudo().get_param( + 'export_task_count')) + 1) + return res + + def unlink(self): + """ Overrides the unlink method of task to delete a task in Jira when + we delete the task in Odoo """ + for task in self: + if task.stage_id and task.stage_id.fold: + raise Warning(_('You cannot delete a task in a folded stage.')) + jira_connection = self.env['ir.config_parameter'].sudo().get_param( + 'odoo_jira_connector.connection') + if jira_connection: + jira_url = self.env['ir.config_parameter'].sudo().get_param( + 'odoo_jira_connector.url', '') + user = self.env['ir.config_parameter'].sudo().get_param( + 'odoo_jira_connector.user_id_jira') + password = self.env['ir.config_parameter'].sudo().get_param( + 'odoo_jira_connector.api_token') + if task.task_id_jira: + requests.delete( + jira_url + '/rest/api/3/issue/' + task.task_id_jira, + headers=HEADERS, auth=(user, password)) + return super(ProjectTask, self).unlink() + + def write(self, vals): + """ Overrides the write method of task to update a task's name in + Jira when we update the task in Odoo""" + jira_connection = self.env['ir.config_parameter'].sudo().get_param( + 'odoo_jira_connector.connection') + + if jira_connection: + jira_url = self.env['ir.config_parameter'].sudo().get_param( + 'odoo_jira_connector.url', '') + user = self.env['ir.config_parameter'].sudo().get_param( + 'odoo_jira_connector.user_id_jira') + password = self.env['ir.config_parameter'].sudo().get_param( + 'odoo_jira_connector.api_token') + + for task in self: + if task.task_id_jira and 'name' in vals: + new_task_name = vals['name'] + payload = { + "fields": { + "summary": new_task_name + } + } + requests.put( + jira_url + '/rest/api/3/issue/' + task.task_id_jira, + json=payload, headers=HEADERS, auth=(user, password)) + return super(ProjectTask, self).write(vals) + + def webhook_data_handle(self, jira_data, webhook_event): + """Function to Handle Jira Data Received from Webhook""" + if webhook_event == 'project_created': + self.create_project(jira_data) + elif webhook_event == 'project_updated': + self.update_project(jira_data) + elif webhook_event == 'project_soft_deleted': + self.delete_project(jira_data) + elif webhook_event == 'jira:issue_created': + self.create_task(jira_data) + elif webhook_event == 'jira:issue_deleted': + self.delete_task(jira_data) + elif webhook_event == 'comment_created': + self.create_comment(jira_data) + elif webhook_event == 'comment_deleted': + self.delete_comment(jira_data) + elif webhook_event == 'user_created': + self.create_user(jira_data) + elif webhook_event == 'user_deleted': + self.delete_user(jira_data) + elif webhook_event == 'board_configuration_changed': + self.board_configuration_change(jira_data) + elif webhook_event == 'jira:issue_updated': + self.update_task(jira_data) + elif webhook_event == 'attachment_deleted': + self.delete_attachment(jira_data) + elif webhook_event == 'sprint_started': + self.sprint_started(jira_data) + elif webhook_event == 'sprint_closed': + self.sprint_closed(jira_data) + + def create_project(self, jira_data): + """function to create project based on webhook response""" + jira_project = jira_data['project'] + existing_project = self.env['project.project'].sudo().search( + [('project_id_jira', '=', jira_project['id'])]) + values = { + 'name': jira_project['name'], + 'project_id_jira': jira_project['id'], + 'jira_project_key': jira_project['key'] + } + if not existing_project: + imported_project = self.env['project.project'].sudo().create( + values) + url = self.env['ir.config_parameter'].sudo().get_param( + 'odoo_jira_connector.url') + user = self.env['ir.config_parameter'].sudo().get_param( + 'odoo_jira_connector.user_id_jira') + password = self.env['ir.config_parameter'].sudo().get_param( + 'odoo_jira_connector.api_token') + auth = HTTPBasicAuth(user, password) + headers = { + "Accept": "application/json" + } + + response = requests.request( + "GET", + url + "/rest/api/3/project/" + jira_project['key'], + headers=headers, + auth=auth + ) + data = response.json() + style_value = data.get('style') + if style_value == 'classic': + imported_project.write({'sprint_active': False}) + else: + imported_project.write({'sprint_active': True}) + + def update_project(self, jira_data): + """function to update project based on webhook response""" + project_id = jira_data['project']['id'] + existing_project = self.env['project.project'].sudo().search( + [('project_id_jira', '=', project_id)]) + if existing_project.name != jira_data['project']['name']: + existing_project.write({'name': jira_data['project']['name']}) + + def delete_project(self, jira_data): + """function to delete project based on webhook response""" + project_id = (jira_data['project']['id']) + self.env['project.project'].sudo().search( + [('project_id_jira', '=', project_id)]).unlink() + + def create_task(self, jira_data): + """function to create task based on webhook response""" + task_name = jira_data['issue']['fields']['summary'] + task_key = jira_data['issue']['key'] + jira_project_id = jira_data['issue']['fields']['project']['id'] + project = self.env['project.project'].sudo().search( + [('project_id_jira', '=', int(jira_project_id))]) + existing_task = self.env['project.task'].sudo().search( + [('task_id_jira', '=', jira_data['issue']['key'])]) + if not existing_task: + self.env['project.task'].sudo().create({ + 'project_id': project.id, + 'name': task_name, + 'task_id_jira': task_key + }) + + def delete_task(self, jira_data): + """function to delete task based on webhook response""" + task_key = jira_data['issue']['key'] + self.env['project.task'].sudo().search( + [('task_id_jira', '=', task_key)]).unlink() + + def create_comment(self, jira_data): + """function to create comment based on webhook response""" + text = jira_data['comment']['body'] + task_key = jira_data['issue']['key'] + task = self.env['project.task'].sudo().search( + [('task_id_jira', '=', task_key)]) + existing_message = self.env['mail.message'].sudo().search( + ['&', ('res_id', '=', task.id), + ('model', '=', 'project.task'), + ('message_id_jira', '=', jira_data['comment']['id'])]) + if not existing_message: + input_string = str(text) + parts = input_string.split(".") + if len(parts) > 1: + body = parts[1] + else: + body = parts[0] + self.env['mail.message'].sudo().create( + {"body": html2plaintext(body), + 'model': 'project.task', + 'res_id': task.id, + 'message_id_jira': jira_data['comment']['id'] + }) + + def delete_comment(self, jira_data): + """function to delete comment based on webhook response""" + self.env['mail.message'].sudo().search( + [('message_id_jira', '=', + jira_data['comment']['id'])]).unlink() + + def create_user(self, jira_data): + """function to create user based on webhook response""" + existing_user = self.env['res.user']. \ + search([('jira_user_key', '=', jira_data['user']['accountId'])]) + if not existing_user: + self.env['res.users'].sudo().create({ + 'login': jira_data['user']['displayName'], + 'name': jira_data['user']['displayName'], + 'jira_user_key': jira_data['user']['accountId'] + }) + + def delete_user(self, jira_data): + """function to delete user based on webhook response""" + self.env['res.users'].sudo().search( + [('jira_user_key', '=', jira_data['accountId'])]).unlink() + + def board_configuration_change(self, jira_data): + """function to create stages or write project into stages based on + webhook response""" + columns = jira_data['configuration']['columnConfig']['columns'] + if jira_data['configuration'].get('location'): + project_key = jira_data['configuration']['location']['key'] + project = self.env['project.project'].sudo().search( + [('jira_project_key', '=', project_key)]) + sequence_value = 1 + for column in columns: + if column['name'] != 'Backlog': + stages_jira_id = column['statuses'][0]['id'] + existing_stage = self.env[ + 'project.task.type'].sudo().search( + [('stages_jira_id', '=', stages_jira_id)]) + existing_stage.write({'project_ids': project, + 'sequence': sequence_value}) + if not existing_stage: + values = { + 'name': column['name'], + 'stages_jira_id': stages_jira_id, + 'jira_project_key': project_key, + 'project_ids': project, + 'sequence': sequence_value, + } + self.env['project.task.type'].sudo().create( + values) + sequence_value += 1 + project.write({'board_id_jira': jira_data['configuration']['id']}) + else: + board_id_jira = jira_data['configuration']['id'] + project = self.env['project.project'].search( + [('board_id_jira', '=', board_id_jira)]) + existing_stages = self.env[ + 'project.task.type'].sudo().search( + [('project_ids', 'in', project.id), + ('stages_jira_id', '!=', '0')]) + jira_status_ids = [] + for column in columns: + for status in column['statuses']: + jira_status_ids.append(status['id']) + if len(jira_status_ids) < len(existing_stages.ids): + removed_stage = self.env[ + 'project.task.type'].sudo().search( + [('project_ids', 'in', project.id), + ('stages_jira_id', 'not in', jira_status_ids)]) + removed_stage.unlink() + elif len(jira_status_ids) > len(existing_stages.ids): + columns = jira_data['configuration']['columnConfig'][ + 'columns'] + num_stages = len(columns) + stage_id = columns[num_stages - 1]['statuses'][0]['id'] + values = { + 'name': columns[num_stages - 1]['name'], + 'stages_jira_id': stage_id, + 'project_ids': project, + 'sequence': num_stages, + } + self.env['project.task.type'].sudo().create(values) + + def update_task(self, jira_data): + """function to update a task, which includes changing the task stage, + adding attachments, adding a description to the task, + changing the task's name, + and adding a sprint based on webhook response""" + task_key = jira_data['issue']['key'] + imported_task = self.env['project.task'].sudo().search( + [('task_id_jira', '=', task_key)]) + to_value = jira_data['changelog']['items'][0]['to'] + if jira_data['changelog']['items'][0]['field'] == 'resolution': + second_to_value = jira_data['changelog']['items'][1]['to'] + task_stage = self.env['project.task.type'].sudo().search( + [('stages_jira_id', '=', second_to_value)]) + imported_task.write({'stage_id': task_stage.id}) + elif jira_data['changelog']['items'][0]['field'] == 'status': + task_stage = self.env['project.task.type'].sudo().search( + [('stages_jira_id', '=', to_value)]) + imported_task.write({'stage_id': task_stage.id}) + elif jira_data['changelog']['items'][0]['field'] == 'Attachment': + if jira_data['changelog']['items'][0]['to'] != 'None': + attachments = jira_data["issue"]['fields']['attachment'] + jira_attachment_id = [attachment['id'] for attachment in + attachments] + num_attachments = len(jira_attachment_id) + user_name = self.env[ + 'ir.config_parameter'].sudo().get_param( + 'odoo_jira_connector.user_id_jira') + api_token = self.env[ + 'ir.config_parameter'].sudo().get_param( + 'odoo_jira_connector.api_token') + auth = HTTPBasicAuth(user_name, api_token) + if num_attachments > 0: + name = attachments[num_attachments - 1].get('filename') + mime_type = attachments[num_attachments - 1].get( + 'mimeType') + src = attachments[num_attachments - 1].get('content') + jira_id = attachments[num_attachments - 1].get('id') + image = base64.b64encode( + requests.get(src, auth=auth).content) + existing_attachments = self.env[ + 'ir.attachment'].sudo().search( + [('res_id', '=', imported_task.id), + ('res_model', '=', 'project.task'), + ('attachment_id_jira', '=', jira_id)] + ) + values = { + 'name': name, + 'type': 'binary', + 'datas': image, + 'res_model': 'project.task', + 'res_id': imported_task.id, + 'mimetype': mime_type, + 'attachment_id_jira': jira_id + } + if not existing_attachments: + self.env['ir.attachment'].sudo().create(values) + else: + pass + elif jira_data['changelog']['items'][0]['field'] == 'description': + imported_task.update({'description': jira_data['changelog'] + ['items'][0]['toString']}) + elif jira_data['changelog']['items'][0]['field'] == 'summary': + if imported_task.name != jira_data['changelog']['items'][0] \ + ['toString']: + imported_task.write( + {'name': jira_data['changelog']['items'][0] + ['toString']}) + elif jira_data['changelog']['items'][0]['field'] == 'Sprint': + project_key = jira_data['issue']['fields']['project']['key'] + project = self.env['project.project'].sudo().search( + [('jira_project_key', '=', project_key)]) + custom_field = jira_data['issue']['fields']['customfield_10020'] + if len(custom_field) > 1: + jira_sprint = self.env['jira.sprint'].sudo().search( + [('sprint_id_jira', '=', + custom_field[len(custom_field) - 1]['id'])]) + if not jira_sprint: + vals = { + 'name': custom_field[len(custom_field) - 1]['name'], + 'sprint_id_jira': + custom_field[len(custom_field) - 1]['id'], + 'project_id': project.id + } + sprint = self.env['jira.sprint'].sudo().create(vals) + if project.task_ids: + for rec in project.task_ids: + rec.write({'sprint_id': sprint.id}) + else: + jira_sprint = self.env['jira.sprint'].sudo().search([( + 'sprint_id_jira', '=', custom_field[0]['id'])]) + if not jira_sprint: + vals = { + 'name': custom_field[0]['name'], + 'sprint_id_jira': custom_field[0]['id'], + 'project_id': project.id + } + sprint = self.env['jira.sprint'].sudo().create(vals) + if project.task_ids: + for rec in project.task_ids: + rec.write({'sprint_id': sprint.id}) + if rec.task_id_jira != task_key: + self.create({ + 'project_id': project.id, + 'name': jira_data['issue']['fields'][ + 'summary'], + 'task_id_jira': task_key, + 'sprint_id': jira_sprint.id + }) + break + else: + task_name = jira_data['issue']['fields']['summary'] + self.create({ + 'project_id': project.id, + 'name': task_name, + 'task_id_jira': task_key, + 'sprint_id': sprint.id + }) + else: + if project.task_ids: + for rec in project.task_ids: + rec.write({'sprint_id': jira_sprint.id}) + if rec.task_id_jira != task_key: + self.create({ + 'project_id': project.id, + 'name': jira_data['issue']['fields'][ + 'summary'], + 'task_id_jira': task_key, + 'sprint_id': jira_sprint.id + }) + break + else: + task_name = jira_data['issue']['fields']['summary'] + self.create({ + 'project_id': project.id, + 'name': task_name, + 'task_id_jira': task_key, + 'sprint_id': jira_sprint.id + }) + + def delete_attachment(self, jira_data): + """function to delete attachment based on the response received from + webhook""" + jira_id = jira_data['attachment']['id'] + self.env['ir.attachment'].sudo().search( + [('attachment_id_jira', '=', jira_id)]).unlink() + + def sprint_started(self, jira_data): + """function to start sprint which is created using webhook response""" + sprint_in_odoo = self.env['jira.sprint'].sudo().search( + [('sprint_id_jira', '=', jira_data['sprint']['id'])]) + if sprint_in_odoo: + start_date = jira_data['sprint']['startDate'] + input_start_date = datetime. \ + strptime(start_date, '%Y-%m-%dT%H:%M:%S.%fZ') + jira_start_date = input_start_date.strftime( + '%Y-%m-%d %H:%M:%S') + end_date = jira_data['sprint']['endDate'] + input_end_date = datetime. \ + strptime(end_date, '%Y-%m-%dT%H:%M:%S.%fZ') + jira_end_date = input_end_date.strftime( + '%Y-%m-%d %H:%M:%S') + sprint_in_odoo.write({ + 'start_date': jira_start_date, + 'end_date': jira_end_date, + 'sprint_goal': jira_data['sprint']['goal'], + 'state': 'ongoing' + }) + + def sprint_closed(self, jira_data): + """function to close sprint which is created using webhook response""" + sprint_in_odoo = self.env['jira.sprint'].sudo().search( + [('sprint_id_jira', '=', jira_data['sprint']['id'])]) + if sprint_in_odoo: + sprint_in_odoo.write({'state': 'completed'}) + self.env['project.task'].sudo().search( + [('stage_id.name', '=', 'Done'), + ('sprint_id', '=', sprint_in_odoo.id)]).unlink() diff --git a/odoo_jira_connector/models/project_task_type.py b/odoo_jira_connector/models/project_task_type.py new file mode 100644 index 000000000..b556bce92 --- /dev/null +++ b/odoo_jira_connector/models/project_task_type.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +################################################################################ +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies() +# Author: Rosmy John (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +################################################################################ +import json +import requests +from requests.auth import HTTPBasicAuth +from odoo import models, fields, api + + +class ProjectTaskType(models.Model): + """This class is inherited for adding some extra field and override the + create function + Methods: + create(vals): + extends create() to export tasks stages to Jira""" + _inherit = 'project.task.type' + + stages_jira_id = fields.Integer(string="Jira ID", + help="Jira id for task stages.", + readonly=True) + jira_project_key = fields.Char(string='Jira Project Key', + help='Corresponding project key of Jira.', + readonly=True) + jira_stages_category = fields.Selection([ + ('TO_DO', 'TO_DO'), + ('IN_PROGRESS', 'IN_PROGRESS'), + ('DONE', 'DONE')], + default='IN_PROGRESS', + string="Jira Status Category", help="Here we can choose the category " + "and the Stage will create in " + "jira under the chosen category.") + + @api.model_create_multi + def create(self, vals): + """ Override the create method of tasks stages to export + tasks stages to Jira """ + stages = super(ProjectTaskType, self).create(vals) + for stage in stages: + if stage.stages_jira_id == 0 and len(stage.project_ids) == 1: + ir_config_parameter = self.env['ir.config_parameter'].sudo() + if ir_config_parameter.get_param('odoo_jira_connector' + '.connection'): + url = ir_config_parameter.get_param('odoo_jira_connector' + '.url', + False) + user = ir_config_parameter.get_param( + 'odoo_jira_connector.user_id_jira', False) + password = ir_config_parameter.get_param( + 'odoo_jira_connector.api_token', False) + auth = HTTPBasicAuth(user, password) + if stage.project_ids[0].sprint_active: + payload = json.dumps({ + "scope": { + "project": { + "id": str(stage.project_ids[0]. + project_id_jira) + }, + "type": "PROJECT" + }, + "statuses": [ + { + "description": "The issue is resolved", + "name": stages.name, + "statusCategory": str( + stage.jira_stages_category), + } + ] + }) + else: + payload = json.dumps({ + "scope": { + "type": "GLOBAL" + }, + "statuses": [ + { + "description": "The issue is resolved", + "name": stage.name, + "statusCategory": str( + stage.jira_stages_category), + } + ] + }) + headers = { + "Accept": "application/json", + "Content-Type": "application/json" + } + requests.request( + "POST", + url + "rest/api/3/statuses", + data=payload, + headers=headers, + auth=auth + ) + return stages diff --git a/odoo_jira_connector/models/res_config_settings.py b/odoo_jira_connector/models/res_config_settings.py new file mode 100755 index 000000000..01522ab1b --- /dev/null +++ b/odoo_jira_connector/models/res_config_settings.py @@ -0,0 +1,585 @@ +# -*- coding: utf-8 -*- +############################################################################### +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies() +# Author: Rosmy John (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +############################################################################### +import base64 +import json +import os +import re +import requests +from requests.auth import HTTPBasicAuth +from odoo import fields, models, _ +from odoo.exceptions import ValidationError +from odoo.tools import html2plaintext +# The Header parameters +HEADERS = { + 'Accept': 'application/json', + 'Content-Type': 'application/json' +} +JIRA_HEADERS = { + 'Accept': 'application/json' +} +ATTACHMENT_HEADERS = { + 'X-Atlassian-Token': 'no-check' +} + + +class ResConfigSettings(models.TransientModel): + """ This class is inheriting the model res.config.settings It contains + fields and functions for the model. + Methods: + get_values(): + extends get_values() to include new config parameters + set_values(): + extends set_values() to include new config parameters + action_test_connection(): + action to perform when clicking on the 'Test Connection' + button. + action_export_to_jira(): + action to perform when clicking on the 'Export/Sync Project' + button. + action_import_from_jira(): + action to perform when clicking on the 'Export Users' button. + action_export_users(): + action to perform when clicking on the 'Reset to Draft' button. + action_import_users(): + action to perform when clicking on the 'Import Users' button. + _export_attachments(attachments, attachment_url): + it is used to export the given attachments to Jira. + find_attachment_type(attachment): + it is used to find the attachment type for the given attachment. + """ + _inherit = 'res.config.settings' + + url = fields.Char( + string='URL', config_parameter='odoo_jira_connector.url', + help='Your Jira URL: E.g. https://yourname.atlassian.net/') + user_id_jira = fields.Char( + string='User Name', help='E.g. yourmail@gmail.com ', + config_parameter='odoo_jira_connector.user_id_jira') + api_token = fields.Char(string='API Token', help='API token in your Jira.', + config_parameter='odoo_jira_connector.api_token') + connection = fields.Boolean( + string='Connection', default=False, help='To identify the connection.', + config_parameter='odoo_jira_connector.connection') + export_project_count = fields.Integer( + string='Export Project Count', default=0, readonly=True, + help='Number of export projects.', + config_parameter='odoo_jira_connector.export_project_count') + export_task_count = fields.Integer( + string='Export Task Count', default=0, readonly=True, + help='Number of export tasks.', + config_parameter='odoo_jira_connector.export_task_count') + import_project_count = fields.Integer( + string='Import Project Count', default=0, readonly=True, + help='Number of import project.', + config_parameter='odoo_jira_connector.import_project_count') + import_task_count = fields.Integer( + string='Import Task Count', default=0, readonly=True, + help='Number of import tasks.', + config_parameter='odoo_jira_connector.import_task_count') + automatic = fields.Boolean(string='Automatic', + help='to make export/import data automated ' + 'while creating it on configured Jira ' + 'account.', + config_parameter='odoo_jira_connector.automatic') + + def action_test_connection(self): + """ Test the connection to Jira + Raises: ValidationError: If the credentials are invalid. + Returns: + dict: client action for displaying notification + """ + try: + # Create an authentication object, using registered email-ID, and + # token received. + auth = HTTPBasicAuth(self.user_id_jira, self.api_token) + response = requests.request('GET', + self.url + 'rest/api/2/project', + headers=JIRA_HEADERS, auth=auth) + if response.status_code == 200 and 'expand' in response.text: + self.env['ir.config_parameter'].sudo().set_param( + 'odoo_jira_connector.connection', True) + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'type': 'success', + 'message': _( + 'Test connection to Jira successful.'), + 'next': { + 'type': 'ir.actions.act_window_close' + } + } + } + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'type': 'danger', + 'message': _('Please Enter Valid Credentials.'), + 'next': { + 'type': 'ir.actions.act_window_close' + } + } + } + except Exception: + raise ValidationError(_('Please Enter Valid Credentials.')) + + def action_export_to_jira(self): + """ Exporting All The Projects And Corresponding Tasks to Jira, + and updating the project or task on Jira if it is updated in Odoo. + """ + ir_config_parameter = self.env['ir.config_parameter'].sudo() + auth = HTTPBasicAuth(self.user_id_jira, self.api_token) + response = requests.request('GET', self.url + 'rest/api/2/project', + headers=JIRA_HEADERS, auth=auth) + projects = json.dumps(json.loads(response.text), sort_keys=True, + indent=4, separators=(',', ': ')) + project_json = json.loads(projects) + name_list = [project['name'] for project in project_json] + id_list = [project['id'] for project in project_json] + odoo_projects = self.env['project.project'].search( + [('project_id_jira', 'in', id_list)]) + for project in odoo_projects: + if project.jira_project_key: + project_keys = project.jira_project_key + else: + key = project.name.upper() + project_key = key[:3] + '1' + key[-3:] + project_keys = project_key.replace(' ', '') + response = requests.get( + self.url + 'rest/api/3/search', headers=HEADERS, + params={'jql': 'project = %s' % project_keys}, + auth=(self.user_id_jira, self.api_token)) + data = response.json() + issue_keys = [issue.get('key') for issue in data.get('issues', {})] + tasks = self.env['project.task'].search( + [('project_id', '=', project.id)]) + for task in tasks: + attachment_url = self.url + 'rest/api/3/issue/%s/' \ + 'attachments' % task.task_id_jira + comment_url = self.url + 'rest/api/3/issue/%s/comment' % ( + task.task_id_jira) + if str(task.task_id_jira) in issue_keys: + messages = self.env['mail.message'].search( + ['&', ('res_id', '=', task.id), + ('model', '=', 'project.task')]) + attachments = self.env['ir.attachment'].search( + [('res_id', '=', task.id)]) + self._export_attachments(attachments, attachment_url) + response = requests.get( + comment_url, headers=HEADERS, auth=( + self.user_id_jira, self.api_token)) + data = response.json() + jira_comment_list = [] + for comments in data['comments']: + content = comments.get('body', {}).get('content', []) + if content and isinstance(content, list) and content[ + 0].get('type') == 'paragraph': + text = content[0]['content'][0].get('text') + if text: + jira_comment_list.append(str(text)) + odoo_comment_list = [str( + html2plaintext(chat.body)) for chat in messages if + str(html2plaintext( + chat.body)) not in jira_comment_list] + comment_list = list(filter(None, odoo_comment_list)) + if len(comment_list) > 0: + for comment in comment_list: + data = json.dumps({ + 'body': { + 'type': 'doc', + 'version': 1, + 'content': [{ + 'type': 'paragraph', + 'content': [{ + 'text': comment, + 'type': 'text' + }]} + ]} + }) + requests.post( + comment_url, headers=HEADERS, data=data, + auth=(self.user_id_jira, self.api_token)) + else: + payload = json.dumps({ + 'fields': { + 'project': + { + 'key': project_keys + }, + 'summary': task.name, + 'description': task.description, + 'issuetype': { + 'name': 'Task' + } + } + }) + response = requests.post( + self.url + '/rest/api/2/issue', headers=HEADERS, + data=payload, auth=(self.user_id_jira, self.api_token)) + data = response.json() + task.task_id_jira = data['key'] + ir_config_parameter.set_param('odoo_jira_connector.export_task_count', int( + ir_config_parameter.get_param( + 'odoo_jira_connector.export_task_count')) + 1) + messages = self.env['mail.message'].search( + ['&', ('res_id', '=', task.id), + ('model', '=', 'project.task')]) + attachments = self.env['ir.attachment'].search( + [('res_id', '=', task.id)]) + self._export_attachments(attachments, attachment_url) + for chat in messages: + data = json.dumps({ + 'body': { + 'type': 'doc', + 'version': 1, + 'content': [{ + 'type': 'paragraph', + 'content': [{ + 'text': str(html2plaintext(chat.body)), + 'type': 'text'}] + }] + } + }) + requests.post( + comment_url, headers=HEADERS, data=data, + auth=(self.user_id_jira, self.api_token)) + odoo_projects = self.env['project.project'].search( + [('project_id_jira', 'not in', id_list), + ('name', 'not in', name_list)]) + for project in odoo_projects: + key = project.name.upper() + project_key = key[:3] + '1' + key[-3:] + project_keys = project_key.replace(' ', "") + auth = HTTPBasicAuth(self.user_id_jira, self.api_token) + project_payload = { + 'name': project.name, + 'key': project_keys, + 'templateKey': 'com.pyxis.greenhopper.jira:gh-simplified' + '-kanban-classic' + } + response = requests.request( + 'POST', self.url + 'rest/simplified/latest/project', + data=json.dumps(project_payload), headers=HEADERS, auth=auth) + data = response.json() + if 'projectId' in data: + project.write({ + 'project_id_jira': data['projectId'], + 'jira_project_key': data['projectKey'] + }) + ir_config_parameter.set_param( + 'odoo_jira_connector.export_project_count', int(ir_config_parameter.get_param( + 'odoo_jira_connector.export_project_count')) + 1) + # for creating a new task inside the project + tasks = self.env['project.task'].search( + [('project_id', '=', project.id)]) + for task in tasks: + payload = json.dumps({ + 'fields': { + 'project': { + 'key': project_keys + }, + 'summary': task.name, + 'description': task.description, + 'issuetype': { + 'name': 'Task' + } + } + }) + response2 = requests.post( + self.url + '/rest/api/2/issue', headers=HEADERS, + data=payload, auth=(self.user_id_jira, self.api_token)) + data = response2.json() + task.task_id_jira = data['key'] + attachment_url = self.url + 'rest/api/3/issue/%s/' \ + 'attachments' % task.task_id_jira + comment_url = self.url + 'rest/api/3/issue/%s/comment' % ( + task.task_id_jira) + ir_config_parameter.set_param('odoo_jira_connector.export_task_count', int( + ir_config_parameter.get_param('odoo_jira_connector.export_task_count')) + 1) + messages = self.env['mail.message'].search( + ['&', ('res_id', '=', task.id), + ('model', '=', 'project.task')]) + attachments = self.env['ir.attachment'].search( + [('res_id', '=', task.id)]) + self._export_attachments(attachments, attachment_url) + for message in messages: + data = json.dumps({ + 'body': { + 'type': 'doc', + 'version': 1, + 'content': [{ + 'type': 'paragraph', + 'content': [{ + 'text': str( + html2plaintext(message.body)), + 'type': 'text'}] + }] + } + }) + requests.post(comment_url, headers=HEADERS, data=data, + auth=(self.user_id_jira, self.api_token)) + elif 'errors' in data and 'projectName' in data['errors']: + raise ValidationError( + "A project with the names already exists in Jira. Please " + "rename the project to export as a new project.") + elif 'errors' in data and 'projectKey' in data['errors']: + raise ValidationError(data['errors']['projectKey']) + + def action_import_from_jira(self): + """ Import all the projects and corresponding tasks + from Odoo to Jira. If a project or task is modified in Odoo, + it will also be updated in Jira. + """ + ir_config_parameter = self.env['ir.config_parameter'].sudo() + auth = HTTPBasicAuth(self.user_id_jira, self.api_token) + response = requests.request( + 'GET', self.url + 'rest/api/2/project', headers=JIRA_HEADERS, + auth=auth) + projects = json.dumps(json.loads(response.text), sort_keys=True, + indent=4, separators=(',', ': ')) + project_json = json.loads(projects) + value_list = [a_dict['key'] for a_dict in project_json] + name_list = [a_dict['name'] for a_dict in project_json] + id_list = [a_dict['id'] for a_dict in project_json] + odoo_projects = self.env['project.project'].search([]) + for (project, key, jira_id) in zip(name_list, value_list, id_list): + jira_project_ids = [ + project.project_id_jira for project in odoo_projects] + if int(jira_id) in jira_project_ids: + response = requests.get( + self.url + 'rest/api/3/search', headers=HEADERS, + params={'jql': 'project = %s' % key}, + auth=(self.user_id_jira, self.api_token)) + data = response.json() + project = self.env['project.project'].search( + [('project_id_jira', '=', int(jira_id))]) + tasks = self.env['project.task'].search( + [('project_id', '=', project.id)]) + task_jira_ids = [task.task_id_jira for task in tasks] + for issue in data['issues']: + comment_url = self.url + 'rest/api/3/issue/%s/' \ + 'comment' % issue['key'] + if issue['key'] in task_jira_ids: + task_id = self.env['project.task'].search( + [('task_id_jira', '=', issue['key'])]) + response = requests.get( + comment_url, headers=HEADERS, + auth=(self.user_id_jira, self.api_token)) + data = response.json() + messages = self.env['mail.message'].search( + ['&', ('res_id', '=', task_id.id), + ('model', '=', 'project.task')]) + odoo_comment_list = [str(html2plaintext( + chat.body)) for chat in messages] + jira_comment_list = [str( + comments['body']['content'][0]['content'][0][ + 'text']) for comments in + data['comments'] if str( + comments['body']['content'][0]['content'][0][ + 'text']) not in odoo_comment_list] + comment_list = list(filter(None, jira_comment_list)) + + if len(comment_list) > 0: + for comment in comment_list: + task_id.message_post(body=str(comment)) + else: + task_id = self.env['project.task'].create({ + 'project_id': project.id, + 'name': issue['fields']['summary'], + 'task_id_jira': issue['key'] + }) + ir_config_parameter.set_param( + 'odoo_jira_connector.import_task_count', int( + ir_config_parameter.get_param( + 'odoo_jira_connector.import_task_count')) + 1) + messages = self.env['mail.message'].search( + ['&', ('res_id', '=', task_id.id), + ('model', '=', 'project.task')]) + response = requests.get( + comment_url, headers=HEADERS, + auth=(self.user_id_jira, self.api_token)) + data = response.json() + odoo_comment_list = [str( + html2plaintext(chat.body)) for chat in messages] + jira_comment_list = [str( + comments['body']['content'][0]['content'][0][ + 'text']) for comments in + data['comments'] if str( + comments['body']['content'][0]['content'][0][ + 'text']) not in odoo_comment_list] + comment_list = list(filter(None, jira_comment_list)) + if len(comment_list) > 0: + for comment in comment_list: + task_id.message_post(body=str(comment)) + else: + project = self.env['project.project'].create({ + 'name': project, + 'project_id_jira': jira_id, + 'jira_project_key': key + }) + ir_config_parameter.set_param( + 'odoo_jira_connector.import_project_count', int(ir_config_parameter.get_param( + 'odoo_jira_connector.import_project_count')) + 1) + response = requests.get( + self.url + 'rest/api/3/search', headers=HEADERS, + params={'jql': 'project = %s' % key}, + auth=(self.user_id_jira, self.api_token)) + data = response.json() + for issue in data['issues']: + task_id = self.env['project.task'].create({ + 'project_id': project.id, + 'name': issue['fields']['summary'], + 'task_id_jira': issue['key'] + }) + rec = ir_config_parameter.set_param( + 'odoo_jira_connector.import_task_count', int(ir_config_parameter.get_param( + 'odoo_jira_connector.import_task_count')) + 1) + messages = self.env['mail.message'].search( + ['&', ('res_id', '=', task_id.id), + ('model', '=', 'project.task')]) + comment_url = self.url + 'rest/api/3/issue/%s/' \ + 'comment' % issue['key'] + response = requests.get( + comment_url, headers=HEADERS, + auth=(self.user_id_jira, self.api_token)) + data = response.json() + odoo_comment_list = [str(html2plaintext( + chat.body)) for chat in messages] + jira_comment_list = [str( + comments['body']['content'][0]['content'][0][ + 'text']) for comments in + data['comments'] if str( + comments['body']['content'][0]['content'][0][ + 'text']) not in odoo_comment_list] + comment_list = list(filter(None, jira_comment_list)) + if len(comment_list) > 0: + for comment in comment_list: + task_id.message_post(body=str(comment)) + + def action_export_users(self): + """ Exporting all the users from Odoo to Jira, and updating the user's + information on Jira if it has been updated in Odoo + Raises: ValidationError: If the credentials are not valid. + """ + response = requests.get( + self.url + 'rest/api/2/users/search', headers=HEADERS, + auth=(self.user_id_jira, self.api_token)) + data = response.json() + issue_keys = [issue['accountId'] for issue in data] + users = self.env['res.users'].search( + [('jira_user_key', 'in', issue_keys)]) + non_jira_users = self.env['res.users'].search( + [('jira_user_key', 'not in', issue_keys)]) + if users: + for user_data in data: + for user in users: + if user_data['accountId'] == user.jira_user_key: + user_data.update({ + 'displayName': user.name + }) + if non_jira_users: + regex = '^\S+@\S+\.\S+$' + for user in non_jira_users: + objs = re.search(regex, user.login) + if objs: + if objs.string == str(user.login): + payload = json.dumps({ + 'emailAddress': user.login, + 'displayName': user.name, + 'name': user.name + }) + response = requests.post( + self.url + 'rest/api/3/user', headers=HEADERS, + data=payload, + auth=(self.user_id_jira, self.api_token)) + data = response.json() + user.write({ + 'jira_user_key': data['accountId'] + }) + else: + raise ValidationError('Invalid E-mail address.') + + def action_import_users(self): + """ Importing all the users from Jira to Odoo, and updating the user's + information on Odoo if it has been updated in Jira. + """ + response = requests.get( + self.url + 'rest/api/2/users/search', headers=HEADERS, + auth=(self.user_id_jira, self.api_token)) + data = response.json() + for user_data in data: + users = self.env['res.users'].sudo().search( + [('login', '=', user_data['displayName'])]) + if users: + users.write({ + 'jira_user_key': user_data['accountId'] + }) + else: + self.env['res.users'].create({ + 'login': user_data['displayName'], + 'name': user_data['displayName'], + 'jira_user_key': user_data['accountId'], + }) + + def _export_attachments(self, attachments, attachment_url): + """ To find the corresponding attachment type in the attachment model + Args: + attachments (model.Model): values for creating new records. + attachment_url (str): URL for the attachment. + """ + for attachment in attachments: + attachment_type = self.find_attachment_type(attachment) + if attachment.datas and attachment_type in ('pdf', 'xlsx', 'jpg'): + temp_file_path = f'/tmp/temp.{attachment_type}' + binary_data = base64.b64decode(attachment.datas) + # Save the binary data to a file + with open(temp_file_path, 'wb') as file: + file.write(binary_data) + if attachment_type == 'jpg' and os.path.splitext( + temp_file_path)[1].lower() != '.jpg': + # Rename the saved file to its corresponding JPG file format + file_path = os.path.splitext(temp_file_path)[0] + '.jpg' + os.rename(temp_file_path, file_path) + temp_file_path = file_path + attachment_file = { + 'file': (attachment.name, open(temp_file_path, 'rb')) + } + requests.post(attachment_url, headers=ATTACHMENT_HEADERS, + files=attachment_file, + auth=(self.user_id_jira, self.api_token)) + + def find_attachment_type(self, attachment): + """ To find the corresponding attachment type in the attachment model + Args: + attachment (model.Model): attachment to fetch the type. + Returns: + str: the attachment type + """ + if attachment.mimetype == 'application/pdf': + return 'pdf' + if attachment.mimetype == 'image/png': + return 'jpg' + if attachment.mimetype == 'application/vnd.openxmlformats-' \ + 'officedocument.spreadsheetml.sheet': + return 'xlsx' + return '' \ No newline at end of file diff --git a/odoo_jira_connector/models/res_users.py b/odoo_jira_connector/models/res_users.py new file mode 100755 index 000000000..243cf835c --- /dev/null +++ b/odoo_jira_connector/models/res_users.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +############################################################################### +# +# Cybrosys Technologies Pvt. Ltd. +# +# Copyright (C) 2024-TODAY Cybrosys Technologies() +# Author: Rosmy John (odoo@cybrosys.com) +# +# You can modify it under the terms of the GNU LESSER +# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. +# +# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE +# (LGPL v3) along with this program. +# If not, see . +# +############################################################################### +import json +import re +import requests +from odoo import api, fields, models + + +class ResUsers(models.Model): + """ + This class is inherited for adding an extra field and + override the create function. + Methods: + create(): + extends create(vals_list) for exporting the new users to Jira + """ + _inherit = 'res.users' + + jira_user_key = fields.Char(string='Jira User Key', + help='The user key of Jira.', readonly=True) + + @api.model_create_multi + def create(self, vals_list): + """ Overrides the create method of users for exporting the new users + to Jira """ + ir_config_parameter = self.env['ir.config_parameter'].sudo() + jira_connection = ir_config_parameter.get_param( + 'odoo_jira_connector.connection') + if jira_connection: + user_auth = ir_config_parameter.get_param( + 'odoo_jira_connector.user_id_jira') + password = ir_config_parameter.get_param( + 'odoo_jira_connector.api_token') + users = super(ResUsers, self).create(vals_list) + odoo_user_url = ir_config_parameter.get_param( + 'odoo_jira_connector.url') + 'rest/api/3/user' + odoo_user_headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + } + payload = json.dumps({ + 'emailAddress': users.login, + 'displayName': users.name, + 'name': users.name + }) + match = re.search('^\S+@\S+\.\S+$', users.login) + if match and match.string == str(users.login): + response = requests.post( + odoo_user_url, headers=odoo_user_headers, data=payload, + auth=(user_auth, password)) + data = response.json() + users.write({'jira_user_key': data['accountId']}) + return users + else: + users = super(ResUsers, self).create(vals_list) + for user in users: + # if partner is global we keep it that way + if user.partner_id.company_id: + user.partner_id.company_id = user.company_id + user.partner_id.active = user.active + return users diff --git a/odoo_jira_connector/security/ir.model.access.csv b/odoo_jira_connector/security/ir.model.access.csv new file mode 100644 index 000000000..c436b0ae7 --- /dev/null +++ b/odoo_jira_connector/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_jira_sprint,jira.sprint.access,model_jira_sprint,base.group_user,1,1,1,1 diff --git a/odoo_jira_connector/static/description/assets/icons/check.png b/odoo_jira_connector/static/description/assets/icons/check.png new file mode 100644 index 000000000..c8e85f51d Binary files /dev/null and b/odoo_jira_connector/static/description/assets/icons/check.png differ diff --git a/odoo_jira_connector/static/description/assets/icons/chevron.png b/odoo_jira_connector/static/description/assets/icons/chevron.png new file mode 100644 index 000000000..2089293d6 Binary files /dev/null and b/odoo_jira_connector/static/description/assets/icons/chevron.png differ diff --git a/odoo_jira_connector/static/description/assets/icons/cogs.png b/odoo_jira_connector/static/description/assets/icons/cogs.png new file mode 100644 index 000000000..95d0bad62 Binary files /dev/null and b/odoo_jira_connector/static/description/assets/icons/cogs.png differ diff --git a/odoo_jira_connector/static/description/assets/icons/consultation.png b/odoo_jira_connector/static/description/assets/icons/consultation.png new file mode 100644 index 000000000..8319d4baa Binary files /dev/null and b/odoo_jira_connector/static/description/assets/icons/consultation.png differ diff --git a/odoo_jira_connector/static/description/assets/icons/ecom-black.png b/odoo_jira_connector/static/description/assets/icons/ecom-black.png new file mode 100644 index 000000000..a9385ff13 Binary files /dev/null and b/odoo_jira_connector/static/description/assets/icons/ecom-black.png differ diff --git a/odoo_jira_connector/static/description/assets/icons/education-black.png b/odoo_jira_connector/static/description/assets/icons/education-black.png new file mode 100644 index 000000000..3eb09b27b Binary files /dev/null and b/odoo_jira_connector/static/description/assets/icons/education-black.png differ diff --git a/odoo_jira_connector/static/description/assets/icons/hotel-black.png b/odoo_jira_connector/static/description/assets/icons/hotel-black.png new file mode 100644 index 000000000..130f613be Binary files /dev/null and b/odoo_jira_connector/static/description/assets/icons/hotel-black.png differ diff --git a/odoo_jira_connector/static/description/assets/icons/license.png b/odoo_jira_connector/static/description/assets/icons/license.png new file mode 100644 index 000000000..a5869797e Binary files /dev/null and b/odoo_jira_connector/static/description/assets/icons/license.png differ diff --git a/odoo_jira_connector/static/description/assets/icons/lifebuoy.png b/odoo_jira_connector/static/description/assets/icons/lifebuoy.png new file mode 100644 index 000000000..658d56ccc Binary files /dev/null and b/odoo_jira_connector/static/description/assets/icons/lifebuoy.png differ diff --git a/odoo_jira_connector/static/description/assets/icons/manufacturing-black.png b/odoo_jira_connector/static/description/assets/icons/manufacturing-black.png new file mode 100644 index 000000000..697eb0e9f Binary files /dev/null and b/odoo_jira_connector/static/description/assets/icons/manufacturing-black.png differ diff --git a/odoo_jira_connector/static/description/assets/icons/pos-black.png b/odoo_jira_connector/static/description/assets/icons/pos-black.png new file mode 100644 index 000000000..97c0f90c1 Binary files /dev/null and b/odoo_jira_connector/static/description/assets/icons/pos-black.png differ diff --git a/odoo_jira_connector/static/description/assets/icons/puzzle.png b/odoo_jira_connector/static/description/assets/icons/puzzle.png new file mode 100644 index 000000000..65cf854e7 Binary files /dev/null and b/odoo_jira_connector/static/description/assets/icons/puzzle.png differ diff --git a/odoo_jira_connector/static/description/assets/icons/restaurant-black.png b/odoo_jira_connector/static/description/assets/icons/restaurant-black.png new file mode 100644 index 000000000..4a35eb939 Binary files /dev/null and b/odoo_jira_connector/static/description/assets/icons/restaurant-black.png differ diff --git a/odoo_jira_connector/static/description/assets/icons/service-black.png b/odoo_jira_connector/static/description/assets/icons/service-black.png new file mode 100644 index 000000000..301ab51cb Binary files /dev/null and b/odoo_jira_connector/static/description/assets/icons/service-black.png differ diff --git a/odoo_jira_connector/static/description/assets/icons/trading-black.png b/odoo_jira_connector/static/description/assets/icons/trading-black.png new file mode 100644 index 000000000..9398ba2f1 Binary files /dev/null and b/odoo_jira_connector/static/description/assets/icons/trading-black.png differ diff --git a/odoo_jira_connector/static/description/assets/icons/training.png b/odoo_jira_connector/static/description/assets/icons/training.png new file mode 100644 index 000000000..884ca024d Binary files /dev/null and b/odoo_jira_connector/static/description/assets/icons/training.png differ diff --git a/odoo_jira_connector/static/description/assets/icons/update.png b/odoo_jira_connector/static/description/assets/icons/update.png new file mode 100644 index 000000000..ecbc5a01a Binary files /dev/null and b/odoo_jira_connector/static/description/assets/icons/update.png differ diff --git a/odoo_jira_connector/static/description/assets/icons/user.png b/odoo_jira_connector/static/description/assets/icons/user.png new file mode 100644 index 000000000..6ffb23d9f Binary files /dev/null and b/odoo_jira_connector/static/description/assets/icons/user.png differ diff --git a/odoo_jira_connector/static/description/assets/icons/wrench.png b/odoo_jira_connector/static/description/assets/icons/wrench.png new file mode 100644 index 000000000..6c04dea0f Binary files /dev/null and b/odoo_jira_connector/static/description/assets/icons/wrench.png differ diff --git a/odoo_jira_connector/static/description/assets/misc/Cybrosys R.png b/odoo_jira_connector/static/description/assets/misc/Cybrosys R.png new file mode 100644 index 000000000..da4058087 Binary files /dev/null and b/odoo_jira_connector/static/description/assets/misc/Cybrosys R.png differ diff --git a/odoo_jira_connector/static/description/assets/misc/categories.png b/odoo_jira_connector/static/description/assets/misc/categories.png new file mode 100644 index 000000000..bedf1e0b1 Binary files /dev/null and b/odoo_jira_connector/static/description/assets/misc/categories.png differ diff --git a/odoo_jira_connector/static/description/assets/misc/check-box.png b/odoo_jira_connector/static/description/assets/misc/check-box.png new file mode 100644 index 000000000..42caf24b9 Binary files /dev/null and b/odoo_jira_connector/static/description/assets/misc/check-box.png differ diff --git a/odoo_jira_connector/static/description/assets/misc/compass.png b/odoo_jira_connector/static/description/assets/misc/compass.png new file mode 100644 index 000000000..d5fed8faa Binary files /dev/null and b/odoo_jira_connector/static/description/assets/misc/compass.png differ diff --git a/odoo_jira_connector/static/description/assets/misc/corporate.png b/odoo_jira_connector/static/description/assets/misc/corporate.png new file mode 100644 index 000000000..2eb13edbf Binary files /dev/null and b/odoo_jira_connector/static/description/assets/misc/corporate.png differ diff --git a/odoo_jira_connector/static/description/assets/misc/customer-support.png b/odoo_jira_connector/static/description/assets/misc/customer-support.png new file mode 100644 index 000000000..79efc72ed Binary files /dev/null and b/odoo_jira_connector/static/description/assets/misc/customer-support.png differ diff --git a/odoo_jira_connector/static/description/assets/misc/cybrosys-logo.png b/odoo_jira_connector/static/description/assets/misc/cybrosys-logo.png new file mode 100644 index 000000000..cc3cc0ccf Binary files /dev/null and b/odoo_jira_connector/static/description/assets/misc/cybrosys-logo.png differ diff --git a/odoo_jira_connector/static/description/assets/misc/features.png b/odoo_jira_connector/static/description/assets/misc/features.png new file mode 100644 index 000000000..b41769f77 Binary files /dev/null and b/odoo_jira_connector/static/description/assets/misc/features.png differ diff --git a/odoo_jira_connector/static/description/assets/misc/logo.png b/odoo_jira_connector/static/description/assets/misc/logo.png new file mode 100644 index 000000000..478462d3e Binary files /dev/null and b/odoo_jira_connector/static/description/assets/misc/logo.png differ diff --git a/odoo_jira_connector/static/description/assets/misc/pictures.png b/odoo_jira_connector/static/description/assets/misc/pictures.png new file mode 100644 index 000000000..56d255fe9 Binary files /dev/null and b/odoo_jira_connector/static/description/assets/misc/pictures.png differ diff --git a/odoo_jira_connector/static/description/assets/misc/pie-chart.png b/odoo_jira_connector/static/description/assets/misc/pie-chart.png new file mode 100644 index 000000000..426e05244 Binary files /dev/null and b/odoo_jira_connector/static/description/assets/misc/pie-chart.png differ diff --git a/odoo_jira_connector/static/description/assets/misc/right-arrow.png b/odoo_jira_connector/static/description/assets/misc/right-arrow.png new file mode 100644 index 000000000..730984a06 Binary files /dev/null and b/odoo_jira_connector/static/description/assets/misc/right-arrow.png differ diff --git a/odoo_jira_connector/static/description/assets/misc/star.png b/odoo_jira_connector/static/description/assets/misc/star.png new file mode 100644 index 000000000..2eb9ab29f Binary files /dev/null and b/odoo_jira_connector/static/description/assets/misc/star.png differ diff --git a/odoo_jira_connector/static/description/assets/misc/support.png b/odoo_jira_connector/static/description/assets/misc/support.png new file mode 100644 index 000000000..4f18b8b82 Binary files /dev/null and b/odoo_jira_connector/static/description/assets/misc/support.png differ diff --git a/odoo_jira_connector/static/description/assets/misc/whatsapp.png b/odoo_jira_connector/static/description/assets/misc/whatsapp.png new file mode 100644 index 000000000..d513a5356 Binary files /dev/null and b/odoo_jira_connector/static/description/assets/misc/whatsapp.png differ diff --git a/odoo_jira_connector/static/description/assets/modules/1.jpg b/odoo_jira_connector/static/description/assets/modules/1.jpg new file mode 100644 index 000000000..0b59ec131 Binary files /dev/null and b/odoo_jira_connector/static/description/assets/modules/1.jpg differ diff --git a/odoo_jira_connector/static/description/assets/modules/2.png b/odoo_jira_connector/static/description/assets/modules/2.png new file mode 100644 index 000000000..d3589e8bd Binary files /dev/null and b/odoo_jira_connector/static/description/assets/modules/2.png differ diff --git a/odoo_jira_connector/static/description/assets/modules/3.png b/odoo_jira_connector/static/description/assets/modules/3.png new file mode 100644 index 000000000..4548d977e Binary files /dev/null and b/odoo_jira_connector/static/description/assets/modules/3.png differ diff --git a/odoo_jira_connector/static/description/assets/modules/5.jpg b/odoo_jira_connector/static/description/assets/modules/5.jpg new file mode 100644 index 000000000..14962a750 Binary files /dev/null and b/odoo_jira_connector/static/description/assets/modules/5.jpg differ diff --git a/odoo_jira_connector/static/description/assets/modules/6.jpg b/odoo_jira_connector/static/description/assets/modules/6.jpg new file mode 100644 index 000000000..8b4d469f9 Binary files /dev/null and b/odoo_jira_connector/static/description/assets/modules/6.jpg differ diff --git a/odoo_jira_connector/static/description/assets/modules/banner.jpg b/odoo_jira_connector/static/description/assets/modules/banner.jpg new file mode 100644 index 000000000..efebcb605 Binary files /dev/null and b/odoo_jira_connector/static/description/assets/modules/banner.jpg differ diff --git a/odoo_jira_connector/static/description/assets/screenshots/Projects_in_jira.png b/odoo_jira_connector/static/description/assets/screenshots/Projects_in_jira.png new file mode 100644 index 000000000..72a2811ba Binary files /dev/null and b/odoo_jira_connector/static/description/assets/screenshots/Projects_in_jira.png differ diff --git a/odoo_jira_connector/static/description/assets/screenshots/api_token2.png b/odoo_jira_connector/static/description/assets/screenshots/api_token2.png new file mode 100755 index 000000000..f5201dc6d Binary files /dev/null and b/odoo_jira_connector/static/description/assets/screenshots/api_token2.png differ diff --git a/odoo_jira_connector/static/description/assets/screenshots/creat_api_token.png b/odoo_jira_connector/static/description/assets/screenshots/creat_api_token.png new file mode 100755 index 000000000..79446ebc4 Binary files /dev/null and b/odoo_jira_connector/static/description/assets/screenshots/creat_api_token.png differ diff --git a/odoo_jira_connector/static/description/assets/screenshots/e_exported_stage.png b/odoo_jira_connector/static/description/assets/screenshots/e_exported_stage.png new file mode 100644 index 000000000..8c3f3514f Binary files /dev/null and b/odoo_jira_connector/static/description/assets/screenshots/e_exported_stage.png differ diff --git a/odoo_jira_connector/static/description/assets/screenshots/e_included_automatic.png b/odoo_jira_connector/static/description/assets/screenshots/e_included_automatic.png new file mode 100644 index 000000000..b046efdc0 Binary files /dev/null and b/odoo_jira_connector/static/description/assets/screenshots/e_included_automatic.png differ diff --git a/odoo_jira_connector/static/description/assets/screenshots/e_scrum_with_sprint.png b/odoo_jira_connector/static/description/assets/screenshots/e_scrum_with_sprint.png new file mode 100644 index 000000000..0295a8f19 Binary files /dev/null and b/odoo_jira_connector/static/description/assets/screenshots/e_scrum_with_sprint.png differ diff --git a/odoo_jira_connector/static/description/assets/screenshots/e_sprint_odoo.png b/odoo_jira_connector/static/description/assets/screenshots/e_sprint_odoo.png new file mode 100644 index 000000000..ffff7534e Binary files /dev/null and b/odoo_jira_connector/static/description/assets/screenshots/e_sprint_odoo.png differ diff --git a/odoo_jira_connector/static/description/assets/screenshots/e_sprint_odoo_form.png b/odoo_jira_connector/static/description/assets/screenshots/e_sprint_odoo_form.png new file mode 100644 index 000000000..a4a1b6a63 Binary files /dev/null and b/odoo_jira_connector/static/description/assets/screenshots/e_sprint_odoo_form.png differ diff --git a/odoo_jira_connector/static/description/assets/screenshots/e_sprint_odoo_li.png b/odoo_jira_connector/static/description/assets/screenshots/e_sprint_odoo_li.png new file mode 100644 index 000000000..1931faee6 Binary files /dev/null and b/odoo_jira_connector/static/description/assets/screenshots/e_sprint_odoo_li.png differ diff --git a/odoo_jira_connector/static/description/assets/screenshots/e_stage_odoo.png b/odoo_jira_connector/static/description/assets/screenshots/e_stage_odoo.png new file mode 100644 index 000000000..b8582517b Binary files /dev/null and b/odoo_jira_connector/static/description/assets/screenshots/e_stage_odoo.png differ diff --git a/odoo_jira_connector/static/description/assets/screenshots/e_test_connection.png b/odoo_jira_connector/static/description/assets/screenshots/e_test_connection.png new file mode 100644 index 000000000..e76b05077 Binary files /dev/null and b/odoo_jira_connector/static/description/assets/screenshots/e_test_connection.png differ diff --git a/odoo_jira_connector/static/description/assets/screenshots/hero.gif b/odoo_jira_connector/static/description/assets/screenshots/hero.gif new file mode 100644 index 000000000..74afe34ed Binary files /dev/null and b/odoo_jira_connector/static/description/assets/screenshots/hero.gif differ diff --git a/odoo_jira_connector/static/description/assets/screenshots/heros.gif b/odoo_jira_connector/static/description/assets/screenshots/heros.gif new file mode 100755 index 000000000..6a370ad48 Binary files /dev/null and b/odoo_jira_connector/static/description/assets/screenshots/heros.gif differ diff --git a/odoo_jira_connector/static/description/assets/screenshots/project_admin_user.png b/odoo_jira_connector/static/description/assets/screenshots/project_admin_user.png new file mode 100644 index 000000000..521dd80be Binary files /dev/null and b/odoo_jira_connector/static/description/assets/screenshots/project_admin_user.png differ diff --git a/odoo_jira_connector/static/description/assets/screenshots/queue_job.png b/odoo_jira_connector/static/description/assets/screenshots/queue_job.png new file mode 100644 index 000000000..22d51c20a Binary files /dev/null and b/odoo_jira_connector/static/description/assets/screenshots/queue_job.png differ diff --git a/odoo_jira_connector/static/description/assets/screenshots/v16-hero.gif b/odoo_jira_connector/static/description/assets/screenshots/v16-hero.gif new file mode 100644 index 000000000..ebf092275 Binary files /dev/null and b/odoo_jira_connector/static/description/assets/screenshots/v16-hero.gif differ diff --git a/odoo_jira_connector/static/description/assets/screenshots/webhook.png b/odoo_jira_connector/static/description/assets/screenshots/webhook.png new file mode 100644 index 000000000..db6902bd7 Binary files /dev/null and b/odoo_jira_connector/static/description/assets/screenshots/webhook.png differ diff --git a/odoo_jira_connector/static/description/assets/screenshots/webhook_setup_1.png b/odoo_jira_connector/static/description/assets/screenshots/webhook_setup_1.png new file mode 100644 index 000000000..6a751b451 Binary files /dev/null and b/odoo_jira_connector/static/description/assets/screenshots/webhook_setup_1.png differ diff --git a/odoo_jira_connector/static/description/assets/screenshots/webhook_setup_2.png b/odoo_jira_connector/static/description/assets/screenshots/webhook_setup_2.png new file mode 100644 index 000000000..d044fa0d4 Binary files /dev/null and b/odoo_jira_connector/static/description/assets/screenshots/webhook_setup_2.png differ diff --git a/odoo_jira_connector/static/description/assets/screenshots/webhook_setup_3.png b/odoo_jira_connector/static/description/assets/screenshots/webhook_setup_3.png new file mode 100644 index 000000000..401ffd340 Binary files /dev/null and b/odoo_jira_connector/static/description/assets/screenshots/webhook_setup_3.png differ diff --git a/odoo_jira_connector/static/description/banner.png b/odoo_jira_connector/static/description/banner.png new file mode 100644 index 000000000..139830a51 Binary files /dev/null and b/odoo_jira_connector/static/description/banner.png differ diff --git a/odoo_jira_connector/static/description/icon.png b/odoo_jira_connector/static/description/icon.png new file mode 100644 index 000000000..55350db86 Binary files /dev/null and b/odoo_jira_connector/static/description/icon.png differ diff --git a/odoo_jira_connector/static/description/index.html b/odoo_jira_connector/static/description/index.html new file mode 100644 index 000000000..e8163b07f --- /dev/null +++ b/odoo_jira_connector/static/description/index.html @@ -0,0 +1,635 @@ +
+
+
+
+ +
+
+
+ Community +
+
+ Enterprise +
+ +
+
+
+
+ +
+
+
+

+ Odoo Jira Connector

+

+ Odoo Jira Connector is a valuable integration tool for businesses that use both Odoo and Jira. +

+ +
+
+ + + +
+
+

+ Overview +

+
+ +
+

+ The Odoo Jira Connector offers a range of features, including bidirectional synchronization of data, automatic creation of Jira issues from + Odoo records, and real-time updates of Jira issues in Odoo. To meet the specific needs of any business users can leverage, they can use Odoo + to handle their business. By connecting these two systems, businesses can streamline their project management processes and improve their + overall efficiency.

+
+

+ +
+ + +
+
+

+ Features +

+
+ +
+
+ +
+
+ Export/import all the information of Project and Task. +
+
+
+
+ +
+
+ Projects, Tasks, Attachment, Status, Users are created automatically in Jira when they are created in Odoo, + and these are also created automatically in Odoo when they are created in Jira. +
+
+
+
+ +
+
+ If we delete Task and Attachments in Odoo, it will be automatically removed from Jira, Also if we delete Task and Attachments in Jira, it will be automatically removed from Odoo. +
+
+
+
+ +
+
+ Available in Odoo 15.0 Community and Enterprise. +
+
+
+
+ +
+
+ All the Messages and log notes in the chatter of the Task are automatically added to the comments of corresponding Task in Jira. +
+
+
+
+ +
+
+ Available Sprint feature. +
+
+
+
+ +
+
+ Exporting Attachment from Odoo to Jira. +
+
+
+ +
+
+

+ Screenshots +

+
+
+

+ Jira Api Key Generation

+

+ Go to Jira --> Security --> Create API token.

+ +

Provide a label and click on the Create button.

+ +
+ +
+

+ Settings View

+

+ Project Admin can only access Jira connector in settings.

+ +

First set the URL of Jira, Username and API Token. Then, you can test the connection.

+ +

After that you can export/import or sync all the project, task and users. It also syncs the comments and attachments.

+

You can import projects while creating them in your Jira account using the "Automatic" feature .

+ +

For that you need to create a webhook in your Jira account.

+

WebHook setup

+

Go to Jira --> Settings --> System settings --> WebHooks

+ +

click on create.

+ +

After adding details like 'name' and 'status',Add url "/jira_webhook" with your app's url.

+

Only allowed protocol is HTTPS.

+ +

Then enable events to trigger that webhook.

+ +

After that save the webhook.

+
+ + +
+

+ Jira View

+

+ If the Export/Sync is clicked, all the project and the corresponding tasks, attachments, and the messages in the chatter are exported + to Jira. If the Import Project & Task is clicked, all the projects, corresponding issues and comments are imported to Odoo. We can + also import & export the users from Odoo to Jira. After that, when we create a project or task or add an attachment in the task or add + messages in Odoo it will automatically create in Jira.

+ + +
+
+

+ Sprint In Odoo

+

Sprint feature is Added in Odoo For Team Managed project Which is imported from Jira.

+ +

That project Which created on Jira will be imported in Odoo.

+

you can add a sprint in that project from Jira.

+ +

after start the sprint you can see that sprint in odoo.

+ + +

Sprint feature is only available for imported team managed project which is having scrum template.

+
+
+

+ Stages Exporting

+

Stages can be exported while creating stage for imported project,you can choose the 'Jira status category' from Odoo.

+ +

exported stages can be seen in the workflow of that project,so from there you can add it to the project.

+ +
+
+

Queue Job

+

These OCA Modules are used to makes the webhook response data into separate queues and performs each queue one after another manner + , So It is necessary to install these modules.

+ +
+ + +
+ +
+
+

Suggested Products

+
+ + +
+
+ + + +
+
+
+

Our Services

+
+
+ +
+
+ +
+
+ Odoo + Customization
+
+ +
+
+ +
+
+ Odoo + Implementation
+
+ +
+
+ +
+
+ Odoo + Support
+
+ + +
+
+ +
+
+ Hire + Odoo + Developer
+
+ +
+
+ +
+
+ Odoo + Integration
+
+ +
+
+ +
+
+ Odoo + Migration
+
+ + +
+
+ +
+
+ Odoo + Consultancy
+
+ +
+
+ +
+
+ Odoo + Implementation
+
+ +
+
+ +
+
+ Odoo + Licensing Consultancy
+
+
+
+ + + +
+
+
+

Our Industries

+
+
+ +
+
+ +
+ Trading +
+

+ Easily procure + and + sell your products

+
+
+ +
+
+ +
+ POS +
+

+ Easy + configuration + and convivial experience

+
+
+ +
+
+ +
+ Education +
+

+ A platform for + educational management

+
+
+ +
+
+ +
+ Manufacturing +
+

+ Plan, track and + schedule your operations

+
+
+ +
+
+ +
+ E-commerce & Website +
+

+ Mobile + friendly, + awe-inspiring product pages

+
+
+ +
+
+ +
+ Service Management +
+

+ Keep track of + services and invoice

+
+
+ +
+
+ +
+ Restaurant +
+

+ Run your bar or + restaurant methodically

+
+
+ +
+
+ +
+ Hotel Management +
+

+ An + all-inclusive + hotel management application

+
+
+ +
+
+ + + + + +
+
+
+

Need Help?

+
+
+
+ + +
+ +
+ + +
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+
+
+ + +
\ No newline at end of file diff --git a/odoo_jira_connector/views/jira_sprint_views.xml b/odoo_jira_connector/views/jira_sprint_views.xml new file mode 100644 index 000000000..ac444f5d2 --- /dev/null +++ b/odoo_jira_connector/views/jira_sprint_views.xml @@ -0,0 +1,53 @@ + + + + + jira.sprint.view.tree + jira.sprint + + + + + + + + + + + + jira.sprint.view.form + jira.sprint + +
+
+
+ + +

+ +

+ + + + + + + + + + + +
+
+
+
+
+ + Sprint + jira.sprint + tree,form + +
\ No newline at end of file diff --git a/odoo_jira_connector/views/project_task_type_views.xml b/odoo_jira_connector/views/project_task_type_views.xml new file mode 100644 index 000000000..949080525 --- /dev/null +++ b/odoo_jira_connector/views/project_task_type_views.xml @@ -0,0 +1,16 @@ + + + + + project.task.type.view.form.inherit.odoo.jira.connector + project.task.type + form + + + + + + + + + \ No newline at end of file diff --git a/odoo_jira_connector/views/project_views.xml b/odoo_jira_connector/views/project_views.xml new file mode 100755 index 000000000..b172d4449 --- /dev/null +++ b/odoo_jira_connector/views/project_views.xml @@ -0,0 +1,33 @@ + + + + + project.project.view.form.inherit.odoo.jira.connector + project.project + form + + + + + + + +