@ -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 <https://cybrosys.com/>`__ |
|||
|
|||
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 <https://cybrosys.com/>`__ |
|||
|
|||
Further information |
|||
=================== |
|||
HTML Description: `<static/description/index.html>`__ |
@ -0,0 +1,36 @@ |
|||
# -*- coding: utf-8 -*- |
|||
################################################################################ |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# 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 <http://www.gnu.org/licenses/>. |
|||
# |
|||
################################################################################ |
|||
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") |
@ -0,0 +1,55 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################### |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# 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 <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################### |
|||
{ |
|||
'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' |
|||
} |
@ -0,0 +1,22 @@ |
|||
# -*- coding: utf-8 -*- |
|||
################################################################################ |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# 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 <http://www.gnu.org/licenses/>. |
|||
# |
|||
################################################################################ |
|||
from . import odoo_jira_connector |
@ -0,0 +1,44 @@ |
|||
# -*- coding: utf-8 -*- |
|||
################################################################################ |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# 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 <http://www.gnu.org/licenses/>. |
|||
# |
|||
################################################################################ |
|||
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) |
@ -0,0 +1,6 @@ |
|||
## Module <odoo_jira_connector> |
|||
|
|||
#### 01.08.2024 |
|||
#### Version 15.0.1.0.0 |
|||
#### ADD |
|||
- Initial commit for Odoo Jira Connector |
@ -0,0 +1,28 @@ |
|||
# -*- coding: utf-8 -*- |
|||
################################################################################ |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# 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 <http://www.gnu.org/licenses/>. |
|||
# |
|||
################################################################################ |
|||
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 |
@ -0,0 +1,102 @@ |
|||
# -*- coding: utf-8 -*- |
|||
################################################################################ |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# 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 <http://www.gnu.org/licenses/>. |
|||
# |
|||
################################################################################ |
|||
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() |
@ -0,0 +1,80 @@ |
|||
# -*- coding: utf-8 -*- |
|||
################################################################################ |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# 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 <http://www.gnu.org/licenses/>. |
|||
# |
|||
################################################################################ |
|||
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}" |
|||
} |
@ -0,0 +1,90 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################### |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# 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 <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################### |
|||
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 |
@ -0,0 +1,670 @@ |
|||
# -*- coding: utf-8 -*- |
|||
################################################################################ |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# 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 <http://www.gnu.org/licenses/>. |
|||
# |
|||
################################################################################ |
|||
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() |
@ -0,0 +1,112 @@ |
|||
# -*- coding: utf-8 -*- |
|||
################################################################################ |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# 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 <http://www.gnu.org/licenses/>. |
|||
# |
|||
################################################################################ |
|||
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 |
@ -0,0 +1,585 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################### |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# 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 <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################### |
|||
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 '' |
@ -0,0 +1,80 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################### |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# 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 <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################### |
|||
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 |
|
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 310 B |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 576 B |
After Width: | Height: | Size: 733 B |
After Width: | Height: | Size: 911 B |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 673 B |
After Width: | Height: | Size: 878 B |
After Width: | Height: | Size: 653 B |
After Width: | Height: | Size: 905 B |
After Width: | Height: | Size: 839 B |
After Width: | Height: | Size: 427 B |
After Width: | Height: | Size: 627 B |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 988 B |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 80 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 589 B |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 967 B |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 82 KiB |
After Width: | Height: | Size: 76 KiB |
After Width: | Height: | Size: 92 KiB |
After Width: | Height: | Size: 85 KiB |
After Width: | Height: | Size: 95 KiB |
After Width: | Height: | Size: 73 KiB |
After Width: | Height: | Size: 135 KiB |
After Width: | Height: | Size: 72 KiB |
After Width: | Height: | Size: 74 KiB |
After Width: | Height: | Size: 110 KiB |
After Width: | Height: | Size: 55 KiB |
After Width: | Height: | Size: 92 KiB |
After Width: | Height: | Size: 84 KiB |
After Width: | Height: | Size: 63 KiB |
After Width: | Height: | Size: 54 KiB |
After Width: | Height: | Size: 72 KiB |
After Width: | Height: | Size: 52 KiB |
After Width: | Height: | Size: 327 KiB |
After Width: | Height: | Size: 45 KiB |
After Width: | Height: | Size: 90 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 115 KiB |
After Width: | Height: | Size: 159 KiB |
After Width: | Height: | Size: 127 KiB |
After Width: | Height: | Size: 125 KiB |
After Width: | Height: | Size: 123 KiB |
After Width: | Height: | Size: 45 KiB |
After Width: | Height: | Size: 7.5 KiB |
@ -0,0 +1,635 @@ |
|||
<div class="container" style="padding: 1rem !important; margin-bottom: 1rem !important;"> |
|||
<div class="row"> |
|||
<div class="col-sm-12 col-md-12 col-lg-12 d-flex justify-content-between" style="border-bottom: 1px solid #d5d5d5;"> |
|||
<div class="my-3"> |
|||
<img src="./assets/icons/logo.png" style="width: auto !important; height: 40px !important;"> |
|||
</div> |
|||
<div class="my-3 d-flex align-items-center"> |
|||
<div |
|||
style="background-color: #7C7BAD !important; color: #fff !important; font-weight: 600 !important; padding: 5px 15px 8px !important; margin: 0 5px !important;"> |
|||
<i class="fa fa-check mr-1"></i>Community |
|||
</div> |
|||
<div |
|||
style="background-color: #875A7B !important; color: #fff !important; font-weight: 600 !important; padding: 5px 15px 8px !important; margin: 0 5px !important;"> |
|||
<i class="fa fa-check mr-1"></i>Enterprise |
|||
</div> |
|||
|
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="container" style="padding: 0rem 1.5rem 4rem !important"> |
|||
<div class="row" style="height: 900px !important;"> |
|||
<div class="col-sm-12 col-md-12 col-lg-12" |
|||
style="padding: 4rem 1rem !important; background-color: #714B67 !important; height: 600px !important; border-radius: 20px !important;"> |
|||
<h1 |
|||
style="font-family: 'Montserrat', sans-serif !important; font-weight: 600 !important; color: #FFFFFF !important; font-size: 3.5rem !important; text-align: center !important;"> |
|||
Odoo Jira Connector</h1> |
|||
<p |
|||
style="font-family: 'Montserrat', sans-serif !important; font-weight: 300 !important; color: #FFFFFF !important; font-size: 1.4rem !important; text-align: center !important;"> |
|||
Odoo Jira Connector is a valuable integration tool for businesses that use both Odoo and Jira. |
|||
</p> |
|||
<img src="assets/screenshots/hero.gif" class="img-responsive" width="100%" height="auto" /> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="row"> |
|||
<div class="col-md-12" style="border-bottom: 1px solid #d5d5d5 !important; margin-bottom: 2rem !important"> |
|||
<h2 |
|||
style="font-family: 'Montserrat', sans-serif !important; font-weight: 600 !important; color: #714B67 !important; font-size: 1.5rem !important;"> |
|||
<i class="fa fa-compass mr-2"></i>Explore this module |
|||
</h2> |
|||
<div class="row"> |
|||
<div class="col-md-6"> |
|||
<a href="#overview" style="text-decoration: none !important;"> |
|||
<div class="row" |
|||
style="background-color: #f5f2f5 !important; border-radius: 10px !important; margin: 1rem !important; padding: 1.5em !important; height: 100px !important;"> |
|||
<div class="col-8"> |
|||
<h3 |
|||
style="font-family: 'Montserrat', sans-serif !important; font-weight: 600 !important; color: #714B67 !important; font-size: 1.2rem !important;"> |
|||
Overview</h3> |
|||
<p |
|||
style="font-family: 'Roboto', sans-serif !important; font-weight: 400 !important; color: #714B67 !important; font-size: 0.9rem !important;"> |
|||
Learn more about this module</p> |
|||
</div> |
|||
<div class="col-4 text-right d-flex justify-content-end align-items-center"> |
|||
<i class="fa fa-chevron-right" style="color: #714B67 !important;"></i> |
|||
</div> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
<div class="col-md-6"> |
|||
<a href="#features" style="text-decoration: none !important;"> |
|||
<div class="row" |
|||
style="background-color: #f5f2f5 !important; border-radius: 10px !important; margin: 1rem !important; padding: 1.5em !important; height: 100px !important;"> |
|||
<div class="col-8"> |
|||
<h3 |
|||
style="font-family: 'Montserrat', sans-serif !important; font-weight: 600 !important; color: #714B67 !important; font-size: 1.2rem !important;"> |
|||
Features</h3> |
|||
<p |
|||
style="font-family: 'Roboto', sans-serif !important; font-weight: 400 !important; color: #714B67 !important; font-size: 0.9rem !important;"> |
|||
View features of this module</p> |
|||
</div> |
|||
<div class="col-4 text-right d-flex justify-content-end align-items-center"> |
|||
<i class="fa fa-chevron-right" style="color: #714B67 !important;"></i> |
|||
</div> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
<div class="col-md-6"> |
|||
<a href="#screenshots" style="text-decoration: none !important;"> |
|||
<div class="row" |
|||
style="background-color: #f5f2f5 !important; border-radius: 10px !important; margin: 1rem !important; padding: 1.5em !important; height: 100px !important;"> |
|||
<div class="col-8"> |
|||
<h3 |
|||
style="font-family: 'Montserrat', sans-serif !important; font-weight: 600 !important; color: #714B67 !important; font-size: 1.2rem !important;"> |
|||
Screenshots</h3> |
|||
<p |
|||
style="font-family: 'Roboto', sans-serif !important; font-weight: 400 !important; color: #714B67 !important; font-size: 0.9rem !important;"> |
|||
See key screenshots of this module</p> |
|||
</div> |
|||
<div class="col-4 text-right d-flex justify-content-end align-items-center"> |
|||
<i class="fa fa-chevron-right" style="color: #714B67 !important;"></i> |
|||
</div> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
</div> |
|||
|
|||
<div class="row" id="overview"> |
|||
<div class="col-md-12" style="border-bottom: 1px solid #d5d5d5 !important; margin: 2rem 0 !important"> |
|||
<h2 |
|||
style="font-family: 'Montserrat', sans-serif !important; font-weight: 600 !important; color: #714B67 !important; font-size: 1.5rem !important;"> |
|||
<i class="fa fa-pie-chart mr-2"></i>Overview |
|||
</h2> |
|||
</div> |
|||
|
|||
<div class="col-mg-12 pl-3"> |
|||
<p |
|||
style="font-family: 'Roboto', sans-serif !important; font-weight: 400 !important; color: #282F33 !important; font-size: 1rem !important; line-height: 30px !important;"> |
|||
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.</p> |
|||
</div> |
|||
</p> |
|||
|
|||
</div> |
|||
|
|||
|
|||
<div class="row" id="features"> |
|||
<div class="col-md-12" style="border-bottom: 1px solid #d5d5d5 !important; margin: 2rem 0 !important"> |
|||
<h2 |
|||
style="font-family: 'Montserrat', sans-serif !important; font-weight: 600 !important; color: #714B67 !important; font-size: 1.5rem !important;"> |
|||
<i class="fa fa-star mr-2"></i>Features |
|||
</h2> |
|||
</div> |
|||
|
|||
<div class="col-md-6 pl-3 py-3 d-flex"> |
|||
<div> |
|||
<img src="assets/icons/check.png"> |
|||
</div> |
|||
<div> |
|||
<span style="font-family: 'Montserrat', sans-serif; font-size: 18px; font-weight: bold;">Export/import all the information of Project and Task.</span> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-6 pl-3 py-3 d-flex"> |
|||
<div> |
|||
<img src="assets/icons/check.png"> |
|||
</div> |
|||
<div> |
|||
<span style="font-family: 'Montserrat', sans-serif; font-size: 18px; font-weight: bold;">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.</span> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-6 pl-3 py-3 d-flex"> |
|||
<div> |
|||
<img src="assets/icons/check.png"> |
|||
</div> |
|||
<div> |
|||
<span style="font-family: 'Montserrat', sans-serif; font-size: 18px; font-weight: bold;">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.</span> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-6 pl-3 py-3 d-flex"> |
|||
<div> |
|||
<img src="assets/icons/check.png"> |
|||
</div> |
|||
<div> |
|||
<span style="font-family: 'Montserrat', sans-serif; font-size: 18px; font-weight: bold;">Available in Odoo 15.0 Community and Enterprise.</span> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-6 pl-3 py-3 d-flex"> |
|||
<div> |
|||
<img src="assets/icons/check.png"> |
|||
</div> |
|||
<div> |
|||
<span style="font-family: 'Montserrat', sans-serif; font-size: 18px; font-weight: bold;">All the Messages and log notes in the chatter of the Task are automatically added to the comments of corresponding Task in Jira.</span> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-6 pl-3 py-3 d-flex"> |
|||
<div> |
|||
<img src="assets/icons/check.png"> |
|||
</div> |
|||
<div> |
|||
<span style="font-family: 'Montserrat', sans-serif; font-size: 18px; font-weight: bold;">Available Sprint feature.</span> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-6 pl-3 py-3 d-flex"> |
|||
<div> |
|||
<img src="assets/icons/check.png"> |
|||
</div> |
|||
<div> |
|||
<span style="font-family: 'Montserrat', sans-serif; font-size: 18px; font-weight: bold;">Exporting Attachment from Odoo to Jira.</span> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="row" id="screenshots"> |
|||
<div class="col-md-12" style="border-bottom: 1px solid #d5d5d5 !important; margin: 2rem 0 !important"> |
|||
<h2 |
|||
style="font-family: 'Montserrat', sans-serif !important; font-weight: 600 !important; color: #714B67 !important; font-size: 1.5rem !important;"> |
|||
<i class="fa fa-image mr-2"></i>Screenshots |
|||
</h2> |
|||
</div> |
|||
<div class="col-lg-12 my-2"> |
|||
<h3 style="font-family: 'Montserrat', sans-serif; font-size: 18px; font-weight: bold;"> |
|||
Jira Api Key Generation</h3> |
|||
<p style="font-weight: 400; font-family: 'Montserrat', sans-serif; font-size: 14px;"> |
|||
Go to Jira --> Security --> Create API token.</p> |
|||
<img src="./assets/screenshots/creat_api_token.png" class="img-thumbnail"> |
|||
<p style="font-weight: 400; font-family: 'Montserrat', sans-serif; font-size: 14px;">Provide a label and click on the Create button.</p> |
|||
<img src="./assets/screenshots/api_token2.png" class="img-thumbnail"> |
|||
</div> |
|||
|
|||
<div class="col-lg-12 my-3"> |
|||
<h3 style="font-family: 'Montserrat', sans-serif; font-size: 18px; font-weight: bold;"> |
|||
Settings View</h3> |
|||
<h2 style="font-family: 'Montserrat', sans-serif; font-size: 18px; font-weight: bold;"> |
|||
Project Admin can only access Jira connector in settings.</h2> |
|||
<img src="./assets/screenshots/project_admin_user.png" class="img-thumbnail"> |
|||
<p>First set the URL of Jira, Username and API Token. Then, you can test the connection.</p> |
|||
<img src="assets/screenshots/e_test_connection.png" class="img-thumbnail"> |
|||
<p>After that you can export/import or sync all the project, task and users. It also syncs the comments and attachments.</p> |
|||
<p>You can import projects while creating them in your Jira account using the "Automatic" feature .</p> |
|||
<img src="assets/screenshots/e_included_automatic.png" class="img-thumbnail"> |
|||
<p>For that you need to create a webhook in your Jira account.</p> |
|||
<h3>WebHook setup</h3> |
|||
<p>Go to Jira --> Settings --> System settings --> WebHooks</p> |
|||
<img src="assets/screenshots/webhook.png" class="img-thumbnail"> |
|||
<p>click on create.</p> |
|||
<img src="assets/screenshots/webhook_setup_1.png" class="img-thumbnail"> |
|||
<p>After adding details like 'name' and 'status',Add url "/jira_webhook" with your app's url.</p> |
|||
<h4>Only allowed protocol is HTTPS.</h4> |
|||
<img src="assets/screenshots/webhook_setup_2.png" class="img-thumbnail"> |
|||
<p>Then enable events to trigger that webhook.</p> |
|||
<img src="assets/screenshots/webhook_setup_3.png" class="img-thumbnail"> |
|||
<p>After that save the webhook.</p> |
|||
</div> |
|||
|
|||
|
|||
<div class="col-lg-12 my-3"> |
|||
<h3 style="font-family: 'Montserrat', sans-serif; font-size: 18px; font-weight: bold;"> |
|||
Jira View</h3> |
|||
<p style="font-weight: 400; font-family: 'Montserrat', sans-serif; font-size: 14px;"> |
|||
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.</p> |
|||
|
|||
<img src="./assets/screenshots/Projects_in_jira.png" class="img-thumbnail"> |
|||
</div> |
|||
<div class="col-lg-12 my-3"> |
|||
<h3 style="font-family: 'Montserrat', sans-serif; font-size: 18px; font-weight: bold;"> |
|||
Sprint In Odoo</h3> |
|||
<p>Sprint feature is Added in Odoo For Team Managed project Which is imported from Jira.</p> |
|||
<img src="./assets/screenshots/e_sprint_odoo.png" class="img-thumbnail"> |
|||
<p>That project Which created on Jira will be imported in Odoo.</p> |
|||
<p>you can add a sprint in that project from Jira.</p> |
|||
<img src="./assets/screenshots/e_scrum_with_sprint.png" class="img-thumbnail"> |
|||
<p>after start the sprint you can see that sprint in odoo.</p> |
|||
<img src="assets/screenshots/e_sprint_odoo_li.png" class="img-thumbnail"> |
|||
<img src="assets/screenshots/e_sprint_odoo_form.png" class="img-thumbnail"> |
|||
<h4>Sprint feature is only available for imported team managed project which is having scrum template.</h4> |
|||
</div> |
|||
<div class="col-lg-12 my-3"> |
|||
<h3 style="font-family: 'Montserrat', sans-serif; font-size: 18px; font-weight: bold;"> |
|||
Stages Exporting</h3> |
|||
<p>Stages can be exported while creating stage for imported project,you can choose the 'Jira status category' from Odoo.</p> |
|||
<img src="assets/screenshots/e_stage_odoo.png" class="img-thumbnail"> |
|||
<p>exported stages can be seen in the workflow of that project,so from there you can add it to the project.</p> |
|||
<img src="assets/screenshots/e_exported_stage.png" class="img-thumbnail"> |
|||
</div> |
|||
<div class="col-lg-12 my-3"> |
|||
<h3>Queue Job</h3> |
|||
<p>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.</p> |
|||
<img src="assets/screenshots/queue_job.png" class="img-thumbnail"> |
|||
</div> |
|||
|
|||
|
|||
</div> |
|||
<!-- SUGGESTED PRODUCTS --> |
|||
<div class="row"> |
|||
<div class="col-lg-12 d-flex flex-column justify-content-center" |
|||
style="text-align: center; padding: 2.5rem 1rem !important;"> |
|||
<h2 style="color: #212529 !important;">Suggested Products</h2> |
|||
<hr |
|||
style="border: 3px solid #714B67 !important; background-color: #714B67 !important; width: 80px !important; margin-bottom: 2rem !important;" /> |
|||
|
|||
<div id="demo1" class="row carousel slide" data-ride="carousel"> |
|||
<!-- The slideshow --> |
|||
<div class="carousel-inner"> |
|||
<div class="carousel-item active" style="min-height:0px"> |
|||
<div class="col-xs-12 col-sm-4 col-md-4 mb16 mt16" style="float:left"> |
|||
<a href="https://apps.odoo.com/apps/modules/15.0/export_stockinfo_xls/" target="_blank"> |
|||
<div style="border-radius:10px"> |
|||
<img class="img img-responsive center-block" |
|||
style="border-top-left-radius:10px; border-top-right-radius:10px" |
|||
src="./assets/modules/export_image.png"> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
<div class="col-xs-12 col-sm-4 col-md-4 mb16 mt16" style="float:left"> |
|||
<a href="https://apps.odoo.com/apps/modules/15.0/custom_gantt_view/" target="_blank"> |
|||
<div style="border-radius:10px"> |
|||
<img class="img img-responsive center-block" |
|||
style="border-top-left-radius:10px; border-top-right-radius:10px" |
|||
src="./assets/modules/gantt_image.png"> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
<div class="col-xs-12 col-sm-4 col-md-4 mb16 mt16" style="float:left"> |
|||
<a href="https://apps.odoo.com/apps/modules/15.0/sales_credit_limit/" target="_blank"> |
|||
<div style="border-radius:10px"> |
|||
<img class="img img-responsive center-block" |
|||
style="border-top-left-radius:10px; border-top-right-radius:10px" |
|||
src="./assets/modules/credit_image.png"> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
</div> |
|||
<div class="carousel-item" style="min-height:0px"> |
|||
<div class="col-xs-12 col-sm-4 col-md-4 mb16 mt16" style="float:left"> |
|||
<a href="https://apps.odoo.com/apps/modules/15.0/base_account_budget/" target="_blank"> |
|||
<div style="border-radius:10px"> |
|||
<img class="img img-responsive center-block" |
|||
style="border-top-left-radius:10px; border-top-right-radius:10px" |
|||
src="./assets/modules/budget_image.png"> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
<div class="col-xs-12 col-sm-4 col-md-4 mb16 mt16" style="float:left"> |
|||
<a href="https://apps.odoo.com/apps/modules/15.0/product_to_quotation/" target="_blank"> |
|||
<div style="border-radius:10px"> |
|||
<img class="img img-responsive center-block" |
|||
style="border-top-left-radius:10px; border-top-right-radius:10px" |
|||
src="./assets/modules/quotation_image.png"> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
<div class="col-xs-12 col-sm-4 col-md-4 mb16 mt16" style="float:left"> |
|||
<a href="https://apps.odoo.com/apps/modules/15.0/employee_documents_expiry/" target="_blank"> |
|||
<div style="border-radius:10px"> |
|||
<img class="img img-responsive center-block" |
|||
style="border-top-left-radius:10px; border-top-right-radius:10px" |
|||
src="./assets/modules/employee_image.png"> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<!-- Left and right controls --> |
|||
<a class="carousel-control-prev" href="#demo1" data-slide="prev" style="left:-25px;width: 35px;color: #000;"> |
|||
<span class="carousel-control-prev-icon"><i class="fa fa-chevron-left" style="font-size:24px"></i></span> </a> |
|||
<a class="carousel-control-next" href="#demo1" data-slide="next" style="right:-25px;width: 35px;color: #000;"> |
|||
<span class="carousel-control-next-icon"><i class="fa fa-chevron-right" style="font-size:24px"></i></span> |
|||
</a> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<!-- END OF SUGGESTED PRODUCTS --> |
|||
|
|||
<!-- OUR SERVICES --> |
|||
<section class="container" style="margin-top: 6rem !important;"> |
|||
<div class="row"> |
|||
<div class="col-lg-12 d-flex flex-column justify-content-center align-items-center"> |
|||
<h2 style="color: #212529 !important;">Our Services</h2> |
|||
<hr |
|||
style="border: 3px solid #714B67 !important; background-color: #714B67 !important; width: 80px !important; margin-bottom: 2rem !important;" /> |
|||
</div> |
|||
|
|||
<div class="col-lg-4 d-flex flex-column justify-content-center align-items-center my-4"> |
|||
<div class="d-flex justify-content-center align-items-center mx-3 my-3" |
|||
style="background-color: #1dd1a1 !important; border-radius: 15px !important; height: 80px; width: 80px;"> |
|||
<img src="assets/icons/cogs.png" class="img-responsive" height="48px" width="48px"> |
|||
</div> |
|||
<h6 class="text-center" style="font-family: Montserrat, 'sans-serif' !important; font-weight: bold;"> |
|||
Odoo |
|||
Customization</h6> |
|||
</div> |
|||
|
|||
<div class="col-lg-4 d-flex flex-column justify-content-center align-items-center my-4"> |
|||
<div class="d-flex justify-content-center align-items-center mx-3 my-3" |
|||
style="background-color: #ff6b6b !important; border-radius: 15px !important; height: 80px; width: 80px;"> |
|||
<img src="assets/icons/wrench.png" class="img-responsive" height="48px" width="48px"> |
|||
</div> |
|||
<h6 class="text-center" style="font-family: Montserrat, 'sans-serif' !important; font-weight: bold;"> |
|||
Odoo |
|||
Implementation</h6> |
|||
</div> |
|||
|
|||
<div class="col-lg-4 d-flex flex-column justify-content-center align-items-center my-4"> |
|||
<div class="d-flex justify-content-center align-items-center mx-3 my-3" |
|||
style="background-color: #6462CD !important; border-radius: 15px !important; height: 80px; width: 80px;"> |
|||
<img src="assets/icons/lifebuoy.png" class="img-responsive" height="48px" width="48px"> |
|||
</div> |
|||
<h6 class="text-center" style="font-family: Montserrat, 'sans-serif' !important; font-weight: bold;"> |
|||
Odoo |
|||
Support</h6> |
|||
</div> |
|||
|
|||
|
|||
<div class="col-lg-4 d-flex flex-column justify-content-center align-items-center my-4"> |
|||
<div class="d-flex justify-content-center align-items-center mx-3 my-3" |
|||
style="background-color: #ffa801 !important; border-radius: 15px !important; height: 80px; width: 80px;"> |
|||
<img src="assets/icons/user.png" class="img-responsive" height="48px" width="48px"> |
|||
</div> |
|||
<h6 class="text-center" style="font-family: Montserrat, 'sans-serif' !important; font-weight: bold;"> |
|||
Hire |
|||
Odoo |
|||
Developer</h6> |
|||
</div> |
|||
|
|||
<div class="col-lg-4 d-flex flex-column justify-content-center align-items-center my-4"> |
|||
<div class="d-flex justify-content-center align-items-center mx-3 my-3" |
|||
style="background-color: #54a0ff !important; border-radius: 15px !important; height: 80px; width: 80px;"> |
|||
<img src="assets/icons/puzzle.png" class="img-responsive" height="48px" width="48px"> |
|||
</div> |
|||
<h6 class="text-center" style="font-family: Montserrat, 'sans-serif' !important; font-weight: bold;"> |
|||
Odoo |
|||
Integration</h6> |
|||
</div> |
|||
|
|||
<div class="col-lg-4 d-flex flex-column justify-content-center align-items-center my-4"> |
|||
<div class="d-flex justify-content-center align-items-center mx-3 my-3" |
|||
style="background-color: #6d7680 !important; border-radius: 15px !important; height: 80px; width: 80px;"> |
|||
<img src="assets/icons/update.png" class="img-responsive" height="48px" width="48px"> |
|||
</div> |
|||
<h6 class="text-center" style="font-family: Montserrat, 'sans-serif' !important; font-weight: bold;"> |
|||
Odoo |
|||
Migration</h6> |
|||
</div> |
|||
|
|||
|
|||
<div class="col-lg-4 d-flex flex-column justify-content-center align-items-center my-4"> |
|||
<div class="d-flex justify-content-center align-items-center mx-3 my-3" |
|||
style="background-color: #786fa6 !important; border-radius: 15px !important; height: 80px; width: 80px;"> |
|||
<img src="assets/icons/consultation.png" class="img-responsive" height="48px" width="48px"> |
|||
</div> |
|||
<h6 class="text-center" style="font-family: Montserrat, 'sans-serif' !important; font-weight: bold;"> |
|||
Odoo |
|||
Consultancy</h6> |
|||
</div> |
|||
|
|||
<div class="col-lg-4 d-flex flex-column justify-content-center align-items-center my-4"> |
|||
<div class="d-flex justify-content-center align-items-center mx-3 my-3" |
|||
style="background-color: #f8a5c2 !important; border-radius: 15px !important; height: 80px; width: 80px;"> |
|||
<img src="assets/icons/training.png" class="img-responsive" height="48px" width="48px"> |
|||
</div> |
|||
<h6 class="text-center" style="font-family: Montserrat, 'sans-serif' !important; font-weight: bold;"> |
|||
Odoo |
|||
Implementation</h6> |
|||
</div> |
|||
|
|||
<div class="col-lg-4 d-flex flex-column justify-content-center align-items-center my-4"> |
|||
<div class="d-flex justify-content-center align-items-center mx-3 my-3" |
|||
style="background-color: #e6be26 !important; border-radius: 15px !important; height: 80px; width: 80px;"> |
|||
<img src="assets/icons/license.png" class="img-responsive" height="48px" width="48px"> |
|||
</div> |
|||
<h6 class="text-center" style="font-family: Montserrat, 'sans-serif' !important; font-weight: bold;"> |
|||
Odoo |
|||
Licensing Consultancy</h6> |
|||
</div> |
|||
</div> |
|||
</section> |
|||
<!-- END OF END OF OUR SERVICES --> |
|||
|
|||
<!-- OUR INDUSTRIES --> |
|||
<section class="container" style="margin-top: 6rem !important;"> |
|||
<div class="row"> |
|||
<div class="col-lg-12 d-flex flex-column justify-content-center align-items-center"> |
|||
<h2 style="color: #212529 !important;">Our Industries</h2> |
|||
<hr |
|||
style="border: 3px solid #714B67 !important; background-color: #714B67 !important; width: 80px !important; margin-bottom: 2rem !important;" /> |
|||
</div> |
|||
|
|||
<div class="col-lg-3"> |
|||
<div class="my-4 d-flex flex-column justify-content-center" |
|||
style="background-color: #f6f8f9 !important; border-radius: 10px; padding: 2rem !important; height: 250px !important;"> |
|||
<img src="./assets/icons/trading-black.png" class="img-responsive mb-3" height="48px" width="48px"> |
|||
<h5 style="font-family: Montserrat, sans-serif !important; color: #000 !important; font-weight: bold;"> |
|||
Trading |
|||
</h5> |
|||
<p style="font-family: Montserrat, sans-serif !important; font-size: 0.9rem !important;"> |
|||
Easily procure |
|||
and |
|||
sell your products</p> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="col-lg-3"> |
|||
<div class="my-4 d-flex flex-column justify-content-center" |
|||
style="background-color: #f6f8f9 !important; border-radius: 10px; padding: 2rem !important; height: 250px !important;"> |
|||
<img src="./assets/icons/pos-black.png" class="img-responsive mb-3" height="48px" width="48px"> |
|||
<h5 style="font-family: Montserrat, sans-serif !important; color: #000 !important; font-weight: bold;"> |
|||
POS |
|||
</h5> |
|||
<p style="font-family: Montserrat, sans-serif !important; font-size: 0.9rem !important;"> |
|||
Easy |
|||
configuration |
|||
and convivial experience</p> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="col-lg-3"> |
|||
<div class="my-4 d-flex flex-column justify-content-center" |
|||
style="background-color: #f6f8f9 !important; border-radius: 10px; padding: 2rem !important; height: 250px !important;"> |
|||
<img src="./assets/icons/education-black.png" class="img-responsive mb-3" height="48px" width="48px"> |
|||
<h5 style="font-family: Montserrat, sans-serif !important; color: #000 !important; font-weight: bold;"> |
|||
Education |
|||
</h5> |
|||
<p style="font-family: Montserrat, sans-serif !important; font-size: 0.9rem !important;"> |
|||
A platform for |
|||
educational management</p> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="col-lg-3"> |
|||
<div class="my-4 d-flex flex-column justify-content-center" |
|||
style="background-color: #f6f8f9 !important; border-radius: 10px; padding: 2rem !important; height: 250px !important;"> |
|||
<img src="./assets/icons/manufacturing-black.png" class="img-responsive mb-3" height="48px" width="48px"> |
|||
<h5 style="font-family: Montserrat, sans-serif !important; color: #000 !important; font-weight: bold;"> |
|||
Manufacturing |
|||
</h5> |
|||
<p style="font-family: Montserrat, sans-serif !important; font-size: 0.9rem !important;"> |
|||
Plan, track and |
|||
schedule your operations</p> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="col-lg-3"> |
|||
<div class="my-4 d-flex flex-column justify-content-center" |
|||
style="background-color: #f6f8f9 !important; border-radius: 10px; padding: 2rem !important; height: 250px !important;"> |
|||
<img src="./assets/icons/ecom-black.png" class="img-responsive mb-3" height="48px" width="48px"> |
|||
<h5 style="font-family: Montserrat, sans-serif !important; color: #000 !important; font-weight: bold;"> |
|||
E-commerce & Website |
|||
</h5> |
|||
<p style="font-family: Montserrat, sans-serif !important; font-size: 0.9rem !important;"> |
|||
Mobile |
|||
friendly, |
|||
awe-inspiring product pages</p> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="col-lg-3"> |
|||
<div class="my-4 d-flex flex-column justify-content-center" |
|||
style="background-color: #f6f8f9 !important; border-radius: 10px; padding: 2rem !important; height: 250px !important;"> |
|||
<img src="./assets/icons/service-black.png" class="img-responsive mb-3" height="48px" width="48px"> |
|||
<h5 style="font-family: Montserrat, sans-serif !important; color: #000 !important; font-weight: bold;"> |
|||
Service Management |
|||
</h5> |
|||
<p style="font-family: Montserrat, sans-serif !important; font-size: 0.9rem !important;"> |
|||
Keep track of |
|||
services and invoice</p> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="col-lg-3"> |
|||
<div class="my-4 d-flex flex-column justify-content-center" |
|||
style="background-color: #f6f8f9 !important; border-radius: 10px; padding: 2rem !important; height: 250px !important;"> |
|||
<img src="./assets/icons/restaurant-black.png" class="img-responsive mb-3" height="48px" width="48px"> |
|||
<h5 style="font-family: Montserrat, sans-serif !important; color: #000 !important; font-weight: bold;"> |
|||
Restaurant |
|||
</h5> |
|||
<p style="font-family: Montserrat, sans-serif !important; font-size: 0.9rem !important;"> |
|||
Run your bar or |
|||
restaurant methodically</p> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="col-lg-3"> |
|||
<div class="my-4 d-flex flex-column justify-content-center" |
|||
style="background-color: #f6f8f9 !important; border-radius: 10px; padding: 2rem !important; height: 250px !important;"> |
|||
<img src="./assets/icons/hotel-black.png" class="img-responsive mb-3" height="48px" width="48px"> |
|||
<h5 style="font-family: Montserrat, sans-serif !important; color: #000 !important; font-weight: bold;"> |
|||
Hotel Management |
|||
</h5> |
|||
<p style="font-family: Montserrat, sans-serif !important; font-size: 0.9rem !important;"> |
|||
An |
|||
all-inclusive |
|||
hotel management application</p> |
|||
</div> |
|||
</div> |
|||
|
|||
</div> |
|||
</section> |
|||
|
|||
<!-- END OF END OF OUR INDUSTRIES --> |
|||
|
|||
<!-- FOOTER --> |
|||
<!-- Footer Section --> |
|||
<section class="container" style="margin: 5rem auto 2rem;"> |
|||
<div class="row" style="max-width:1540px;"> |
|||
<div class="col-lg-12 d-flex flex-column justify-content-center align-items-center"> |
|||
<h2 style="color: #212529 !important;">Need Help?</h2> |
|||
<hr |
|||
style="border: 3px solid #714B67 !important; background-color: #714B67 !important; width: 80px !important; margin-bottom: 2rem !important;" /> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Contact Cards --> |
|||
<div class="row d-flex justify-content-center align-items-center" |
|||
style="max-width:1540px; margin: 0 auto 2rem auto;"> |
|||
|
|||
<div class="col-lg-12" style="padding: 0rem 3rem 2rem; border-radius: 10px; margin-right: 3rem; "> |
|||
|
|||
<div class="row mt-4"> |
|||
<div class="col-lg-6"> |
|||
<a href="mailto:odoo@cybrosys.com" target="_blank" class="btn btn-block mb-2 deep_hover" |
|||
style="text-decoration: none; background-color: #4d4d4d; color: #FFF; border-radius: 4px;"><i |
|||
class="fa fa-envelope mr-2"></i>odoo@cybrosys.com</a> |
|||
</div> |
|||
<div class="col-lg-6"> |
|||
<a href="https://api.whatsapp.com/send?phone=918606827707" target="_blank" |
|||
class="btn btn-block mb-2 deep_hover" |
|||
style="text-decoration: none; background-color: #25D366; color: #FFF; border-radius: 4px;"><i |
|||
class="fa fa-whatsapp mr-2"></i>+91 86068 27707</a> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
</div> |
|||
<!-- End of Contact Cards --> |
|||
</section> |
|||
<!-- Footer --> |
|||
<section class="oe_container" style="padding: 2rem 3rem 1rem;"> |
|||
<div class="row" style="max-width:1540px; margin: 0 auto; margin-right: 3rem; "> |
|||
<!-- Logo --> |
|||
<div class="col-lg-12 d-flex justify-content-center align-items-center" style="margin-top: 3rem;"> |
|||
<img src="https://www.cybrosys.com/images/logo.png" width="200px" height="auto" /> |
|||
</div> |
|||
<!-- End of Logo --> |
|||
<div class="col-lg-12"> |
|||
<hr |
|||
style="margin-top: 3rem;background: linear-gradient(90deg, rgba(2,0,36,0) 0%, rgba(229,229,229,1) 33%, rgba(229,229,229,1) 58%, rgba(0,212,255,0) 100%); height: 2px; border-style: none;"> |
|||
<!-- End of Footer Section --> |
|||
</div> |
|||
</div> |
|||
</section> |
|||
<!-- END OF FOOTER --> |
|||
|
|||
</div> |
@ -0,0 +1,53 @@ |
|||
<?xml version="1.0" encoding="UTF-8" ?> |
|||
<odoo> |
|||
<!-- SPRINT TREE VIEW--> |
|||
<record id="jira_sprint_view_tree" model="ir.ui.view"> |
|||
<field name="name">jira.sprint.view.tree</field> |
|||
<field name="model">jira.sprint</field> |
|||
<field name="arch" type="xml"> |
|||
<tree> |
|||
<field name="name" /> |
|||
<field name="sprint_goal"/> |
|||
<field name="start_date"/> |
|||
<field name="end_date"/> |
|||
</tree> |
|||
</field> |
|||
</record> |
|||
<!-- SPRINT FORM VIEW--> |
|||
<record id="jira_sprint_view_form" model="ir.ui.view"> |
|||
<field name="name">jira.sprint.view.form</field> |
|||
<field name="model">jira.sprint</field> |
|||
<field name="arch" type="xml"> |
|||
<form> |
|||
<header> |
|||
<button string="Tasks" class="oe_stat_button" type="object" name="action_get_tasks"/> |
|||
<button string="Backlogs" class="oe_stat_button" type="object" name="action_get_backlogs"/> |
|||
<button string="All tasks" class="oe_stat_button" type="object" name="action_get_all_tasks"/> |
|||
</header> |
|||
<sheet> |
|||
<group> |
|||
<h3> |
|||
<field name="name" placeholder="Sprint name...." help="Enter name of the sprint."/> |
|||
</h3> |
|||
<group> |
|||
<field name="start_date" help="Start date of sprint."/> |
|||
<field name="end_date" help="End date of sprint."/> |
|||
<field name="project_id" help="Related project."/> |
|||
<field name="sprint_id_jira" help="Sprint id of Jira."/> |
|||
</group> |
|||
<notebook> |
|||
<page string="Goals"> |
|||
<field name="sprint_goal" widget="html" help="Goals of the sprint."/> |
|||
</page> |
|||
</notebook> |
|||
</group> |
|||
</sheet> |
|||
</form> |
|||
</field> |
|||
</record> |
|||
<record id="jira_sprint_view_action" model="ir.actions.act_window"> |
|||
<field name="name">Sprint</field> |
|||
<field name="res_model">jira.sprint</field> |
|||
<field name="view_mode">tree,form</field> |
|||
</record> |
|||
</odoo> |
@ -0,0 +1,16 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<odoo> |
|||
<!-- Inheriting project.task.type form view and adding new field to project_id_jira --> |
|||
<record id="task_type_edit" model="ir.ui.view"> |
|||
<field name="name">project.task.type.view.form.inherit.odoo.jira.connector</field> |
|||
<field name="model">project.task.type</field> |
|||
<field name="type">form</field> |
|||
<field name="inherit_id" ref="project.task_type_edit"/> |
|||
<field name="arch" type="xml"> |
|||
<field name="project_ids" position="after"> |
|||
<field name="stages_jira_id" help="stage id of Jira."/> |
|||
<field name="jira_stages_category" help="Category of stages in jira."/> |
|||
</field> |
|||
</field> |
|||
</record> |
|||
</odoo> |
@ -0,0 +1,33 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<odoo> |
|||
<!-- Inheriting project.project form view and adding new field to project_id_jira --> |
|||
<record id="edit_project" model="ir.ui.view"> |
|||
<field name="name">project.project.view.form.inherit.odoo.jira.connector</field> |
|||
<field name="model">project.project</field> |
|||
<field name="type">form</field> |
|||
<field name="inherit_id" ref="project.edit_project"/> |
|||
<field name="arch" type="xml"> |
|||
<field name="user_id" position="after"> |
|||
<field name="project_id_jira"/> |
|||
</field> |
|||
<xpath expr="//button[@name='%(project.project_milestone_all)d']" position="before"> |
|||
<field name="sprint_active" invisible="1"/> |
|||
<button string="Sprint" class="oe_stat_button" type="object" name="action_get_sprint" icon="fa-clock-o" attrs="{'invisible':[('sprint_active', '!=' ,True)]}"/> |
|||
</xpath> |
|||
</field> |
|||
</record> |
|||
<!-- Inheriting the project.task form view and adding new field task_id_jira --> |
|||
<record id="view_task_form2" model="ir.ui.view"> |
|||
<field name="name">project.task.view.form.inherit.odoo.jira.connector</field> |
|||
<field name="model">project.task</field> |
|||
<field name="type">form</field> |
|||
<field name="inherit_id" ref="project.view_task_form2"/> |
|||
<field name="arch" type="xml"> |
|||
<field name="tag_ids" position="after"> |
|||
<field name="task_id_jira" help="id of task in jira."/> |
|||
<field name="sprint_id" attrs="{'invisible':[('task_sprint_active', '!=' ,True)]}" help="related sprint."/> |
|||
<field name="task_sprint_active" invisible="1"/> |
|||
</field> |
|||
</field> |
|||
</record> |
|||
</odoo> |
@ -0,0 +1,82 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<odoo> |
|||
<!-- Inheriting the config setting and add the fields and buttons for connecting to Jira --> |
|||
<record id="res_config_settings_view_form" model="ir.ui.view"> |
|||
<field name="name">res.config.settings.view.form.inherit.odoo.jira.connector</field> |
|||
<field name="model">res.config.settings</field> |
|||
<field name="priority" eval="20"/> |
|||
<field name="inherit_id" ref="base_setup.res_config_settings_view_form"/> |
|||
<field name="arch" type="xml"> |
|||
<xpath expr="//div[hasclass('settings')]" position="inside"> |
|||
<div class="app_settings_block" data-string="Jira Connector" string="Jira Connector" data-key="odoo_jira_connector" groups="project.group_project_manager"> |
|||
<h2>Jira Connector</h2> |
|||
<div class="row mt16 o_settings_container"> |
|||
<div class="col-xs-12 col-md-6 o_setting_box" id="print_node_settings"> |
|||
<div class="o_setting_right_pane"> |
|||
<div class="content-group"> |
|||
<div class="row mt8"> |
|||
<label class="col-lg-3" string="URL" for="url"/> |
|||
<field name="url" help="Url of Jira."/> |
|||
</div> |
|||
<div class="row mt8"> |
|||
<label class="col-lg-3" string="User Name" for="user_id_jira"/> |
|||
<field name="user_id_jira" help="Enter User name in Jira."/> |
|||
</div> |
|||
<div class="row mt8"> |
|||
<label class="col-lg-3" string="API Token" for="api_token"/> |
|||
<field name="api_token" help="Enter api token."/> |
|||
</div> |
|||
<div class="row mt8"> |
|||
<label class="col-lg-3" string="Automatic" for="automatic" attrs="{'invisible':[('connection', '!=' ,True)]}"/> |
|||
<field name="automatic" attrs="{'invisible':[('connection', '!=' ,True)]}" help="Using this field we can automate the project management in Jira ,Eg:we can create project in odoo while creating it on Jira."/> |
|||
</div> |
|||
<div class="row mt8"> |
|||
<label class="col-lg-3" string="Export Project Count" for="export_project_count"/> |
|||
<field name="export_project_count" help="count of exported project."/> |
|||
</div> |
|||
<div class="row mt8"> |
|||
<label class="col-lg-3" string="Export Task Count" for="export_task_count"/> |
|||
<field name="export_task_count" help="count of exported tasks."/> |
|||
</div> |
|||
<div class="row mt8"> |
|||
<label class="col-lg-3" string="Import Project Count" for="import_project_count"/> |
|||
<field name="import_project_count" help="count of imported project."/> |
|||
</div> |
|||
<div class="row mt8"> |
|||
<label class="col-lg-3" string="Import Task Count" for="import_task_count"/> |
|||
<field name="import_task_count" help="count of imported task."/> |
|||
</div> |
|||
<div class="row mt8"> |
|||
<field name="connection" invisible="1"/> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="col-12 col-lg-6 o_setting_box"> |
|||
<div class="o_setting_right_pane" style="display: flex; flex-wrap: wrap;"> |
|||
<button name="action_test_connection" string="Test Connection" |
|||
attrs="{'invisible':[('connection', '=' ,True)]}" type="object" class="btn-primary"/> |
|||
<div style="margin:10px !important"> |
|||
<button name="action_export_to_jira" string="Export/Sync Project" |
|||
attrs="{'invisible':['|',('connection', '!=' ,True),('automatic', '=' ,True)]}" type="object" class="btn-primary"/> |
|||
</div> |
|||
<div style="margin:10px !important"> |
|||
<button name="action_import_from_jira" string="Import Project & Tasks" |
|||
attrs="{'invisible':['|',('connection', '!=' ,True),('automatic', '=' ,True)]}" type="object" class="btn-primary"/> |
|||
</div> |
|||
<div style="margin:10px !important"> |
|||
<button name="action_export_users" string="Export Users" |
|||
attrs="{'invisible':['|',('connection', '!=' ,True),('automatic', '=' ,True)]}" type="object" class="btn-primary"/> |
|||
</div> |
|||
<div style="margin:10px !important"> |
|||
<button name="action_import_users" string="Import Users" |
|||
attrs="{'invisible':['|',('connection', '!=' ,True),('automatic', '=' ,True)]}" type="object" class="btn-primary"/> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</xpath> |
|||
</field> |
|||
</record> |
|||
</odoo> |
@ -0,0 +1,16 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<odoo> |
|||
<!-- inheriting res.user form view and adding new field --> |
|||
<record id="view_users_form" model="ir.ui.view"> |
|||
<field name="name">res.users.view.form.inherit.odoo.jira.connector</field> |
|||
<field name="model">res.users</field> |
|||
<field name="type">form</field> |
|||
<field name="inherit_id" ref="base.view_users_form"/> |
|||
<field name="arch" type="xml"> |
|||
<field name="login" position="after"> |
|||
<label for="jira_user_key">Jira Key</label> |
|||
<field name="jira_user_key" string="Jira Key" help="Jira user Key." readonly="1"/> |
|||
</field> |
|||
</field> |
|||
</record> |
|||
</odoo> |