@ -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: (V17) Dhanya B, 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,33 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################## |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Dhanya B (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 controllers, models |
|||
from odoo.exceptions import UserError |
|||
|
|||
|
|||
def pre_init_hook(env): |
|||
queue_job = env['ir.model.data'].sudo().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: Dhanya B (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': '17.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.jpg'], |
|||
'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: Dhanya B (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: Dhanya B (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> |
|||
|
|||
#### 30.07.2024 |
|||
#### Version 17.0.1.0.0 |
|||
#### ADD |
|||
- Initial commit for Odoo Jira Connector |
@ -0,0 +1,29 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################## |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Dhanya B (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 |
|||
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: Dhanya B (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 values_list and 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: Dhanya B (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: Dhanya B (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,160 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################## |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Dhanya B (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 api, fields, models, _ |
|||
from odoo.exceptions import ValidationError |
|||
|
|||
# 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.') |
|||
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) |
@ -0,0 +1,537 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################## |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Dhanya B (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.tools import html2plaintext |
|||
|
|||
# The Header parameters |
|||
HEADERS = {'Accept': 'application/json', 'Content-Type': 'application/json'} |
|||
|
|||
|
|||
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': 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.users']. \ |
|||
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: Dhanya B (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,561 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################## |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Dhanya B (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.', |
|||
) |
|||
export_task_count = fields.Integer( |
|||
string='Export Task Count', default=0, readonly=True, |
|||
help='Number of export tasks.', |
|||
) |
|||
import_project_count = fields.Integer( |
|||
string='Import Project Count', |
|||
help='Number of import project.', readonly=True, |
|||
) |
|||
import_task_count = fields.Integer( |
|||
string='Import Task Count', |
|||
help='Number of import tasks.', readonly=True, |
|||
) |
|||
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. |
|||
""" |
|||
|
|||
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'] |
|||
self.env['ir.config_parameter'].sudo().set_param( |
|||
'odoo_jira_connector.export_task_count', int( |
|||
self.env['ir.config_parameter'].sudo().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'] |
|||
}) |
|||
self.env['ir.config_parameter'].sudo().set_param( |
|||
'odoo_jira_connector.export_project_count', |
|||
int(self.env['ir.config_parameter'].sudo().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) |
|||
self.env['ir.config_parameter'].sudo().set_param( |
|||
'odoo_jira_connector.export_task_count', int( |
|||
self.env['ir.config_parameter'].sudo().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_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 '' |
|||
|
|||
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. |
|||
""" |
|||
|
|||
auth = HTTPBasicAuth(self.user_id_jira, self.api_token) |
|||
response = requests.get(self.url + 'rest/api/2/project', |
|||
headers=JIRA_HEADERS, auth=auth) |
|||
projects = json.loads(response.text) |
|||
odoo_projects = self.env['project.project'].search([]) |
|||
|
|||
jira_project_ids = [int(a_dict['id']) for a_dict in projects] |
|||
name_list = [a_dict['name'] for a_dict in projects] |
|||
key_list = [a_dict['key'] for a_dict in projects] |
|||
|
|||
for (name, key, jira_id) in zip(name_list, key_list, jira_project_ids): |
|||
if jira_id in [project.project_id_jira for project in |
|||
odoo_projects]: |
|||
response = requests.get(self.url + 'rest/api/3/search', |
|||
headers=JIRA_HEADERS, |
|||
params={'jql': 'project = %s' % key}, |
|||
auth=auth) |
|||
data = response.json() |
|||
project = self.env['project.project'].search( |
|||
[('project_id_jira', '=', 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 = self.env['project.task'].search( |
|||
[('task_id_jira', '=', issue['key'])]) |
|||
else: |
|||
task = self.env['project.task'].create({ |
|||
'project_id': project.id, |
|||
'name': issue['fields']['summary'], |
|||
'task_id_jira': issue['key'] |
|||
}) |
|||
self.import_task_count += 10 |
|||
|
|||
response = requests.get(comment_url, headers=JIRA_HEADERS, |
|||
auth=auth) |
|||
data = response.json() |
|||
messages = self.env['mail.message'].search( |
|||
[('res_id', '=', task.id), |
|||
('model', '=', 'project.task')]) |
|||
odoo_comment_list = [str(html2plaintext(chat.body)) for chat |
|||
in messages] |
|||
jira_comment_list = [ |
|||
str(comment['body']['content'][0]['content'][0]['text']) |
|||
for comment in data['comments'] if str( |
|||
comment['body']['content'][0]['content'][0][ |
|||
'text']) not in odoo_comment_list] |
|||
comment_list = list(filter(None, jira_comment_list)) |
|||
for comment in comment_list: |
|||
task.message_post(body=comment) |
|||
else: |
|||
project = self.env['project.project'].create({ |
|||
'name': name, |
|||
'project_id_jira': jira_id, |
|||
'jira_project_key': key |
|||
}) |
|||
self.import_project_count = 10 |
|||
response = requests.get(self.url + 'rest/api/3/search', |
|||
headers=JIRA_HEADERS, |
|||
params={'jql': 'project = %s' % key}, |
|||
auth=auth) |
|||
data = response.json() |
|||
|
|||
for issue in data['issues']: |
|||
comment_url = self.url + 'rest/api/3/issue/%s/comment' % \ |
|||
issue['key'] |
|||
task = self.env['project.task'].create({ |
|||
'project_id': project.id, |
|||
'name': issue['fields']['summary'], |
|||
'task_id_jira': issue['key'] |
|||
}) |
|||
self.import_task_count += 1 |
|||
|
|||
response = requests.get(comment_url, headers=JIRA_HEADERS, |
|||
auth=auth) |
|||
data = response.json() |
|||
messages = self.env['mail.message'].search( |
|||
[('res_id', '=', task.id), |
|||
('model', '=', 'project.task')]) |
|||
odoo_comment_list = [str(html2plaintext(chat.body)) for chat |
|||
in messages] |
|||
jira_comment_list = [ |
|||
str(comment['body']['content'][0]['content'][0]['text']) |
|||
for comment in data['comments'] if str( |
|||
comment['body']['content'][0]['content'][0][ |
|||
'text']) not in odoo_comment_list] |
|||
comment_list = list(filter(None, jira_comment_list)) |
|||
|
|||
for comment in comment_list: |
|||
task.message_post(body=comment) |
|||
|
@ -0,0 +1,81 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################## |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Dhanya B (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, |
|||
'products':[] |
|||
}) |
|||
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: 135 KiB |
After Width: | Height: | Size: 72 KiB |
After Width: | Height: | Size: 74 KiB |
After Width: | Height: | Size: 110 KiB |
After Width: | Height: | Size: 92 KiB |
After Width: | Height: | Size: 115 KiB |
After Width: | Height: | Size: 92 KiB |
After Width: | Height: | Size: 94 KiB |
After Width: | Height: | Size: 76 KiB |
After Width: | Height: | Size: 67 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 114 KiB |
After Width: | Height: | Size: 120 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: 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: 52 KiB |
After Width: | Height: | Size: 49 KiB |
After Width: | Height: | Size: 74 KiB |
After Width: | Height: | Size: 284 KiB |
After Width: | Height: | Size: 78 KiB |
After Width: | Height: | Size: 98 KiB |
After Width: | Height: | Size: 100 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: 92 KiB |
After Width: | Height: | Size: 424 KiB |
After Width: | Height: | Size: 92 KiB |
After Width: | Height: | Size: 94 KiB |
After Width: | Height: | Size: 76 KiB |
After Width: | Height: | Size: 67 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 114 KiB |
After Width: | Height: | Size: 120 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: 85 KiB |
After Width: | Height: | Size: 22 KiB |
@ -0,0 +1,779 @@ |
|||
<!DOCTYPE html> |
|||
<html lang="en"> |
|||
<head> |
|||
<meta charset="UTF-8"> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|||
<title>Odoo App 3 Index</title> |
|||
<!-- Bootstrap CSS --> |
|||
<link rel="stylesheet" |
|||
href="https://cdn.jsdelivr.net/npm/bootstrap@4.0.0/dist/css/bootstrap.min.css" |
|||
integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" |
|||
crossorigin="anonymous"> |
|||
<link rel="stylesheet" |
|||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/all.min.css"> |
|||
<link rel="preconnect" href="https://fonts.googleapis.com"> |
|||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
|||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" |
|||
rel="stylesheet"> |
|||
</head> |
|||
<body> |
|||
<section> |
|||
<div class="container" |
|||
style="font-family: 'Inter', sans-serif !important;background-color: #fff !important;"> |
|||
<div class="row"> |
|||
<div class="col-sm-12 col-md-12 col-lg-12 d-flex justify-content-between flex-wrap align-items-sm-center" |
|||
style="border-bottom:1px solid rgba(0, 0, 0, 0.22)"> |
|||
<div class="my-3"> |
|||
<img src="assets/misc/Cybrosys R.png" |
|||
style="width:auto !important; height:40px !important"> |
|||
</div> |
|||
<div class="my-3 d-flex align-items-center"> |
|||
<div class="text-center" |
|||
style="background-color:#017E84 !important;font-size: 0.8rem !important; color:#fff !important; font-weight:500 !important; padding:4px !important; margin:0 3px !important; border-radius:50px !important; min-width: 120px !important;"> |
|||
Community |
|||
</div> |
|||
<div class="text-center" |
|||
style="background-color:#875A7B !important; color:#fff !important;font-size: 0.8rem !important; font-weight:500 !important; padding:4px !important; margin:0 3px !important; border-radius:50px !important;min-width: 120px !important;"> |
|||
Enterprise |
|||
</div> |
|||
<div class="text-center" |
|||
style="background-color:#7C7BAD !important; color:#fff !important;font-size: 0.8rem !important; font-weight:500 !important; padding:4px !important; margin:0 3px !important; border-radius:50px !important; min-width: 120px !important;"> |
|||
Odoo.sh |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="row"> |
|||
<div class="col-sm-12 col-md-12 col-lg-12 text-center d-flex align-items-center flex-column" |
|||
style="margin: 80px 0px !important;"> |
|||
<h1 style="font-size: 2.8rem;font-weight: 700; color: |
|||
#1A202C;"> |
|||
Odoo Jira Connector</h1> |
|||
<p class="my-3 mb-4" |
|||
style="max-width: 80%; font-weight: 400 !important; line-height: 32px; color: #718096;"> |
|||
Odoo Jira Connector is a valuable integration tool for businesses that use both Odoo and Jira. </p> |
|||
<div style="width: 80%; margin-top: 3rem;"> |
|||
<img src="assets/screenshots/hero.gif" |
|||
class="img-responsive" width="100%" height="auto"> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="container mt-5 mb-5"> |
|||
<div class="col-lg-12 d-flex flex-column justify-content-center align-items-center mt-4"> |
|||
<p class="m-0" |
|||
style="font-weight: 600; font-size: 24px; color:#714b67 !important"> |
|||
Key Highlights |
|||
</p> |
|||
</div> |
|||
<div class="row py-4"> |
|||
<div class="col-md-6 col-sm-12 p-3"> |
|||
<div class="d-flex h-100" style="padding: 30px;border-radius: 12px; |
|||
background: #FFF; |
|||
box-shadow: 1px 2px 3px 0px rgba(0, 0, 0, 0.25); "> |
|||
<div style="width: 36px; height: 36px; border-radius: 50%; background: #714B67; |
|||
display: flex; justify-content: center; align-items: center; |
|||
margin-right: 10px; flex-shrink: 0;"> |
|||
<i class="fa-solid fa-star " |
|||
style="color: #fff;font-size:14px;"></i> |
|||
</div> |
|||
<div> |
|||
<p style="color: #1A202C;font-weight: 600; |
|||
font-size: 1.2rem; margin-bottom: 2px;"> |
|||
Export/import all the information of Project and Task.</p> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-6 col-sm-12 p-3"> |
|||
<div class="d-flex h-100" style="padding: 30px;border-radius: 12px; |
|||
background: #FFF; |
|||
box-shadow: 1px 2px 3px 0px rgba(0, 0, 0, 0.25); "> |
|||
<div style="width: 36px; height: 36px; border-radius: 50%; background: #714B67; |
|||
display: flex; justify-content: center; align-items: center; |
|||
margin-right: 10px; flex-shrink: 0;"> |
|||
<i class="fa-solid fa-star " |
|||
style="color: #fff;font-size:14px;"></i> |
|||
</div> |
|||
<div> |
|||
<p style="color: #1A202C;font-weight: 600; |
|||
font-size: 1.2rem; margin-bottom: 2px;"> |
|||
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.</p> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-6 col-sm-12 p-3"> |
|||
<div class="d-flex h-100" style="padding: 30px;border-radius: 12px; |
|||
background: #FFF; |
|||
box-shadow: 1px 2px 3px 0px rgba(0, 0, 0, 0.25); "> |
|||
<div style="width: 36px; height: 36px; border-radius: 50%; background: #714B67; |
|||
display: flex; justify-content: center; align-items: center; |
|||
margin-right: 10px; flex-shrink: 0;"> |
|||
<i class="fa-solid fa-star " |
|||
style="color: #fff;font-size:14px;"></i> |
|||
</div> |
|||
<div> |
|||
<p style="color: #1A202C;font-weight: 600; |
|||
font-size: 1.2rem; margin-bottom: 2px;"> |
|||
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.</p> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-6 col-sm-12 p-3"> |
|||
<div class="d-flex h-100" style="padding: 30px;border-radius: 12px; |
|||
background: #FFF; |
|||
box-shadow: 1px 2px 3px 0px rgba(0, 0, 0, 0.25); "> |
|||
<div style="width: 36px; height: 36px; border-radius: 50%; background: #714B67; |
|||
display: flex; justify-content: center; align-items: center; |
|||
margin-right: 10px; flex-shrink: 0;"> |
|||
<i class="fa-solid fa-star " |
|||
style="color: #fff;font-size:14px;"></i> |
|||
</div> |
|||
<div> |
|||
<p style="color: #1A202C;font-weight: 600; |
|||
font-size: 1.2rem; margin-bottom: 2px;"> |
|||
Available in Odoo 17.0 Community and Enterprise.</p> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-6 col-sm-12 p-3"> |
|||
<div class="d-flex h-100" style="padding: 30px;border-radius: 12px; |
|||
background: #FFF; |
|||
box-shadow: 1px 2px 3px 0px rgba(0, 0, 0, 0.25); "> |
|||
<div style="width: 36px; height: 36px; border-radius: 50%; background: #714B67; |
|||
display: flex; justify-content: center; align-items: center; |
|||
margin-right: 10px; flex-shrink: 0;"> |
|||
<i class="fa-solid fa-star " |
|||
style="color: #fff;font-size:14px;"></i> |
|||
</div> |
|||
<div> |
|||
<p style="color: #1A202C;font-weight: 600; |
|||
font-size: 1.2rem; margin-bottom: 2px;"> |
|||
All the Messages and log notes in the chatter of the Task are automatically added to the |
|||
comments of corresponding Task in Jira.</p> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
|
|||
<div class="col-md-6 col-sm-12 p-3"> |
|||
<div class="d-flex h-100" style="padding: 30px;border-radius: 12px; |
|||
background: #FFF; |
|||
box-shadow: 1px 2px 3px 0px rgba(0, 0, 0, 0.25); "> |
|||
<div style="width: 36px; height: 36px; border-radius: 50%; background: #714B67; |
|||
display: flex; justify-content: center; align-items: center; |
|||
margin-right: 10px; flex-shrink: 0;"> |
|||
<i class="fa-solid fa-star " |
|||
style="color: #fff;font-size:14px;"></i> |
|||
</div> |
|||
<div> |
|||
<p style="color: #1A202C;font-weight: 600; |
|||
font-size: 1.2rem; margin-bottom: 2px;"> |
|||
Available Sprint feature.</p> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
|
|||
<div class="col-md-6 col-sm-12 p-3"> |
|||
<div class="d-flex h-100" style="padding: 30px;border-radius: 12px; |
|||
background: #FFF; |
|||
box-shadow: 1px 2px 3px 0px rgba(0, 0, 0, 0.25); "> |
|||
<div style="width: 36px; height: 36px; border-radius: 50%; background: #714B67; |
|||
display: flex; justify-content: center; align-items: center; |
|||
margin-right: 10px; flex-shrink: 0;"> |
|||
<i class="fa-solid fa-star " |
|||
style="color: #fff;font-size:14px;"></i> |
|||
</div> |
|||
<div> |
|||
<p style="color: #1A202C;font-weight: 600; |
|||
font-size: 1.2rem; margin-bottom: 2px;"> |
|||
Exporting Attachment from Odoo to Jira.</p> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="container rounded"> |
|||
<ul class="nav nav-tabs d-flex" |
|||
style="width: fit-content;margin: 0 auto;gap: 1rem;"> |
|||
<li class="col text-center py-2 text-nowrap " |
|||
style="color: #fff; background-color: #714B67;border-radius: 6px 6px 0px 0px;"> |
|||
<a |
|||
class="active show" data-toggle="tab" href="#tab1" |
|||
style="color: #fff;font-weight: 500; background-color: #714B67; text-decoration: none;"> |
|||
<i class="fa-regular fa-image pr-2" |
|||
style="color: #fff;"></i> |
|||
Screenshots</a></li> |
|||
<li class="col text-center py-2 text-nowrap " |
|||
style="color: #fff; background-color: #714B67;border-radius: 6px 6px 0px 0px;"> |
|||
<a |
|||
data-toggle="tab" href="#tab2" |
|||
style="color: #fff;font-weight: 500; text-decoration: none;"><i |
|||
class="fa-solid fa-star pr-2" |
|||
style="color: #fff;"></i>Features</a></li> |
|||
<li class="col text-center py-2 text-nowrap " |
|||
style="color: #fff; background-color: #714B67;border-radius: 6px 6px 0px 0px;"> |
|||
<a |
|||
data-toggle="tab" href="#tab3" |
|||
style="color: #fff;font-weight: 500; text-decoration: none; background-color: #714B67;"><i |
|||
class="fa-solid fa-book-open pr-2" |
|||
style="color: #fff;"></i>Released Notes</a></li> |
|||
</ul> |
|||
<div class="tab-content" |
|||
style="background-color: rgba(121, 113, 119, 0.04);"> |
|||
<div id="tab1" class="tab-pane fade in active show"> |
|||
<div class="col-lg-12 py-2" |
|||
style="padding: 1rem 4rem !important;"> |
|||
<div |
|||
style="border: 1px solid #d8d6d6; border-radius: 4px; background: #fff; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);"> |
|||
<div class="row justify-content-center p-3 w-100 m-0"> |
|||
</div> |
|||
<div class="px-3"> |
|||
<h4 class="mt-2" |
|||
style=" font-weight:600 !important; color:#282F33 !important; font-size:1.3rem !important"> |
|||
Jira Api Key Generation |
|||
</h4> |
|||
<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> |
|||
</div> |
|||
<div class="col-lg-12 py-2" |
|||
style="padding: 1rem 4rem !important;"> |
|||
<div |
|||
style="border: 1px solid #d8d6d6; border-radius: 4px; background: #fff; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);"> |
|||
<div class="row justify-content-center p-3 w-100 m-0"> |
|||
</div> |
|||
<div class="px-3"> |
|||
<h4 class="mt-2" |
|||
style=" font-weight:600 !important; color:#282F33 !important; font-size:1.3rem !important"> |
|||
Settings View |
|||
</h4> |
|||
<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/jira-qqqqq.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/jira_1.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/jira-wwww.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> |
|||
</div> |
|||
</div> |
|||
<div id="tab4 class=" tab-pane fade in active show |
|||
"> |
|||
<div class="col-lg-12 py-2" |
|||
style="padding: 1rem 4rem !important;"> |
|||
<div |
|||
style="border: 1px solid #d8d6d6; border-radius: 4px; background: #fff; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);"> |
|||
<div class="row justify-content-center p-3 w-100 m-0"> |
|||
</div> |
|||
<div class="px-3"> |
|||
<h4 class="mt-2" |
|||
style=" font-weight:600 !important; color:#282F33 !important; font-size:1.3rem !important"> |
|||
Jira View</h4> |
|||
<p> |
|||
Jira view of Projects. |
|||
.</p> |
|||
<img src="assets/screenshots/Projects_in_jira.png" class="img-thumbnail"> |
|||
<p> |
|||
you can add a sprint in that project from Jira. |
|||
</p> |
|||
<img src="assets/screenshots/sprint.png" class="img-thumbnail"> |
|||
</div> |
|||
<div class="px-3"> |
|||
<h4 class="mt-2" |
|||
style=" font-weight:600 !important; color:#282F33 !important; font-size:1.3rem !important"> |
|||
Jira View</h4> |
|||
<p> |
|||
Jira view of Users. |
|||
.</p> |
|||
<img src="assets/screenshots/user_jira.png" class="img-thumbnail"> |
|||
|
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div id="tab3" class="tab-pane fade"> |
|||
<div class="col-mg-12 active" style="padding: 1rem 4rem;"> |
|||
<div class="py-3" |
|||
style="font-weight: 500;background-color: #fff; border-radius: 4px; padding: 1rem; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);"> |
|||
<div class="d-flex mb-3" |
|||
style="font-size: 0.8rem; font-weight: 500;"><span>Version |
|||
17.0.1.0.0</span><span |
|||
class="px-2">|</span><span |
|||
style="color: #714B67;font-weight: 600;">Released on:15th January 2024</span> |
|||
</div> |
|||
<p class="m-0" |
|||
style=" color:#718096!important; font-size:1rem !important;line-height: 28px;"> |
|||
|
|||
Initial Commit for Odoo Jira Connector.</p> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="container mt-5"> |
|||
<div class="col-lg-12 d-flex flex-column justify-content-center align-items-center mt-5"> |
|||
<p class="m-0" |
|||
style="font-weight: 600; font-size: 24px; color:#000 !important"> |
|||
Related Products</p> |
|||
</div> |
|||
</div> |
|||
<div id="myCarousel" class="carousel slide py-3" data-ride="carousel"> |
|||
<div class="carousel-inner"> |
|||
<div class="carousel-item active"> |
|||
<div class="row p-4"> |
|||
<div class="col"> |
|||
<div class="p-3"> |
|||
<a href="https://apps.odoo.com/apps/modules/17.0/project_task_attachments/" |
|||
style="color: #000; text-decoration: none;"> |
|||
<div style="border:1px solid #CBCBCB !important;border-radius: 4px;"> |
|||
<div style="width: 300px; "> |
|||
<img src="assets/modules/1.png" |
|||
alt="" width="100%" |
|||
height="auto"> |
|||
|
|||
</div> |
|||
<p class="text-center pt-2 text-black font-weight-bold"> |
|||
Project And Task Attachments</p> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
</div> |
|||
<div class="col"> |
|||
<div class="p-3"> |
|||
<a href="https://apps.odoo.com/apps/modules/17.0/project_task_timer/" |
|||
style="color: #000; text-decoration: none;"> |
|||
<div style="border:1px solid #CBCBCB !important;border-radius: 4px;"> |
|||
<div style="width: 300px; "> |
|||
<img src="assets/modules/3.jpg" |
|||
alt="" width="100%" |
|||
height="auto"> |
|||
|
|||
</div> |
|||
<p class="text-center pt-2 text-black font-weight-bold"> |
|||
Project Task Timer</p> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
</div> |
|||
<div class="col"> |
|||
<div class="p-3"> |
|||
<a href="https://apps.odoo.com/apps/modules/17.0/projects_task_checklists/" |
|||
style="color: #000; text-decoration: none;"> |
|||
<div style="border:1px solid #CBCBCB !important;border-radius: 4px;"> |
|||
<div style="width: 300px; "> |
|||
<img src="assets/modules/2.png" |
|||
alt="" width="100%" |
|||
height="auto"> |
|||
|
|||
</div> |
|||
<p class="text-center pt-2 text-black font-weight-bold"> |
|||
Project Task Checklist</p> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="carousel-item"> |
|||
<div class="row p-4"> |
|||
<div class="col"> |
|||
<div class="p-3"> |
|||
<a href="https://apps.odoo.com/apps/modules/17.0/project_tasks_from_templates/" |
|||
style="color: #000; text-decoration: none;"> |
|||
<div style="border:1px solid #CBCBCB !important;border-radius: 4px;"> |
|||
<div style="width: 300px; "> |
|||
<img src="assets/modules/5.jpg" |
|||
alt="" width="100%" |
|||
height="auto"> |
|||
|
|||
</div> |
|||
<p class="text-center pt-2 text-black font-weight-bold"> |
|||
Project Templates</p> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
</div> |
|||
<div class="col"> |
|||
<div class="p-3"> |
|||
<a href="https://apps.odoo.com/apps/modules/17.0/advanced_project_management_system/" |
|||
style="color: #000; text-decoration: none;"> |
|||
<div style="border:1px solid #CBCBCB !important;border-radius: 4px;"> |
|||
<div style="width: 300px;"> |
|||
<img src="assets/modules/7.jpg" |
|||
alt="" width="100%" |
|||
height="auto"> |
|||
|
|||
</div> |
|||
<p class="text-center pt-2 text-black font-weight-bold"> |
|||
Advanced Project Management System</p> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
</div> |
|||
<div class="col"> |
|||
<div class="p-3"> |
|||
<a href="https://apps.odoo.com/apps/modules/17.0/project_report_pdf/" |
|||
style="color: #000; text-decoration: none;"> |
|||
<div style="border:1px solid #CBCBCB !important;border-radius: 4px;"> |
|||
<div style="width: 300px;"> |
|||
<img src="assets/modules/6.png" |
|||
alt="" width="100%" |
|||
height="auto"> |
|||
</div> |
|||
<p class="text-center pt-2 text-black font-weight-bold"> |
|||
Project Report XLS & PDF</p> |
|||
</div> |
|||
</a> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<a class="carousel-control-prev" href="#myCarousel" |
|||
data-slide="prev" style="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="#myCarousel" |
|||
data-slide="next" style="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 class="container mt-5"> |
|||
<div class="col-lg-12 d-flex flex-column justify-content-center align-items-center mt-4"> |
|||
<p class="m-0" |
|||
style="font-weight: 600; font-size: 24px; color:#000 !important"> |
|||
Our Services</p> |
|||
|
|||
</div> |
|||
</div> |
|||
<div class="container my-5"> |
|||
<div class="row py-3"> |
|||
<div class="col-md-4 col-sm-6 px-4 py-4"> |
|||
<div |
|||
style="background-color: #fff; padding: 25px; text-align: center; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, rgba(0, 0, 0, 0.06) 0px 1px 2px 0px; position: relative;border-radius: 4px;"> |
|||
<div style="position: absolute; top: 0%; left: 50%; transform: translate(-50%, -50%);"> |
|||
<div style="background-color:#13EA36 ; border-radius: 50%; padding: 15px; width: 68px; |
|||
height: 68px; display: inline-block; box-shadow:0px 4px 4px rgba(0, 0, 0, 0.25);"> |
|||
<img src="assets/icons/cogs.png" |
|||
alt="service-icon" width="38px" |
|||
height="auto"> |
|||
</div> |
|||
</div> |
|||
<p style="margin-top: 20px; font-weight: bold;">Odoo |
|||
Customization</p> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-4 col-sm-6 px-4 py-4"> |
|||
<div |
|||
style="background-color: #fff; padding: 25px; text-align: center; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, rgba(0, 0, 0, 0.06) 0px 1px 2px 0px; position: relative;border-radius: 4px;"> |
|||
<div style="position: absolute; top: 0%; left: 50%; transform: translate(-50%, -50%);"> |
|||
<div style="background-color:#DBC711; border-radius: 50%; padding: 15px; width: 68px; |
|||
height: 68px; display: inline-block; box-shadow:0px 4px 4px rgba(0, 0, 0, 0.25);"> |
|||
<img src="assets/icons/wrench.png" |
|||
alt="service-icon" width="38px" |
|||
height="auto"> |
|||
</div> |
|||
</div> |
|||
<p style="margin-top: 20px; font-weight: bold;">Odoo |
|||
Implementation</p> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-4 col-sm-6 px-4 py-4"> |
|||
<div |
|||
style="background-color: #fff; padding: 25px; text-align: center; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, rgba(0, 0, 0, 0.06) 0px 1px 2px 0px; position: relative; border-radius: 4px;"> |
|||
<div style="position: absolute; top: 0%; left: 50%; transform: translate(-50%, -50%);"> |
|||
<div style="background-color:#FF6B6B ; border-radius: 50%; padding: 15px; width: 68px; |
|||
height: 68px; display: inline-block; box-shadow:0px 4px 4px rgba(0, 0, 0, 0.25);"> |
|||
<img src="assets/icons/lifebuoy.png" |
|||
alt="service-icon" width="38px" |
|||
height="auto"> |
|||
</div> |
|||
</div> |
|||
<p style="margin-top: 20px; font-weight: bold;">Odoo |
|||
Support</p> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-4 col-sm-6 px-4 py-4"> |
|||
<div |
|||
style="background-color: #fff; padding: 25px; text-align: center; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, rgba(0, 0, 0, 0.06) 0px 1px 2px 0px; position: relative; border-radius: 4px;"> |
|||
<div style="position: absolute; top: 0%; left: 50%; transform: translate(-50%, -50%);"> |
|||
<div style="background-color:#FFA801 ; border-radius: 50%; padding: 15px; width: 68px; |
|||
height: 68px; display: inline-block; box-shadow:0px 4px 4px rgba(0, 0, 0, 0.25);"> |
|||
<img src="assets/icons/user.png" |
|||
alt="service-icon" width="38px" |
|||
height="auto"> |
|||
</div> |
|||
</div> |
|||
<p style="margin-top: 20px; font-weight: bold;">Hire |
|||
Odoo Developer</p> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-4 col-sm-6 px-4 py-4"> |
|||
<div |
|||
style="background-color: #fff; padding: 25px; text-align: center; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, rgba(0, 0, 0, 0.06) 0px 1px 2px 0px; position: relative; border-radius: 4px;"> |
|||
|
|||
<div style="position: absolute; top: 0%; left: 50%; transform: translate(-50%, -50%);"> |
|||
<div style="background-color:#54A0FF; border-radius: 50%; padding: 15px; width: 68px; |
|||
height: 68px; display: inline-block; box-shadow:0px 4px 4px rgba(0, 0, 0, 0.25);"> |
|||
<img src="assets/icons/puzzle.png" |
|||
alt="service-icon" width="38px" |
|||
height="auto"> |
|||
</div> |
|||
</div> |
|||
<p style="margin-top: 20px; font-weight: bold;">Odoo |
|||
Integration</p> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-4 col-sm-6 px-4 py-4"> |
|||
<div |
|||
style="background-color: #fff; padding: 25px; text-align: center; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, rgba(0, 0, 0, 0.06) 0px 1px 2px 0px; position: relative;border-radius: 4px;"> |
|||
<div style="position: absolute; top: 0%; left: 50%; transform: translate(-50%, -50%);"> |
|||
<div style="background-color:#6D7680 ; border-radius: 50%; padding: 15px; width: 68px; |
|||
height: 68px; display: inline-block; box-shadow:0px 4px 4px rgba(0, 0, 0, 0.25);"> |
|||
<img src="assets/icons/update.png" |
|||
alt="service-icon" width="38px" |
|||
height="auto"> |
|||
</div> |
|||
</div> |
|||
<p style="margin-top: 20px; font-weight: bold;">Odoo |
|||
Migration</p> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-4 col-sm-6 px-4 py-4"> |
|||
<div |
|||
style="background-color: #fff; padding: 25px; text-align: center; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, rgba(0, 0, 0, 0.06) 0px 1px 2px 0px; position: relative;border-radius: 4px;"> |
|||
<div style="position: absolute; top: 0%; left: 50%; transform: translate(-50%, -50%);"> |
|||
<div style="background-color:#786FA6 ; border-radius: 50%; padding: 15px; width: 68px; |
|||
height: 68px; display: inline-block; box-shadow:0px 4px 4px rgba(0, 0, 0, 0.25);"> |
|||
<img src="assets/icons/consultation.png" |
|||
alt="service-icon" width="38px" |
|||
height="auto"> |
|||
</div> |
|||
</div> |
|||
<p style="margin-top: 20px; font-weight: bold;">Odoo |
|||
Consultancy</p> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-4 col-sm-6 px-4 py-4"> |
|||
<div |
|||
style="background-color: #fff; padding: 25px; text-align: center; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, rgba(0, 0, 0, 0.06) 0px 1px 2px 0px;position: relative;border-radius: 4px;"> |
|||
<div style="position: absolute; top: 0%; left: 50%; transform: translate(-50%, -50%);"> |
|||
<div style="background-color:#F8A5C2 ; border-radius: 50%; padding: 15px; width: 68px; |
|||
height: 68px; display: inline-block; box-shadow:0px 4px 4px rgba(0, 0, 0, 0.25);"> |
|||
<img src="assets/icons/training.png" |
|||
alt="service-icon" width="38px" |
|||
height="auto"> |
|||
</div> |
|||
</div> |
|||
<p style="margin-top: 20px; font-weight: bold;">Odoo |
|||
Implementation</p> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-4 col-sm-6 px-4 py-4"> |
|||
<div |
|||
style="background-color: #fff; padding: 25px; text-align: center; box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, rgba(0, 0, 0, 0.06) 0px 1px 2px 0px; position: relative;border-radius: 4px;"> |
|||
<div style="position: absolute; top: 0%; left: 50%; transform: translate(-50%, -50%);"> |
|||
<div style="background-color:#E6BE26; border-radius: 50%; padding: 15px; width: 68px; |
|||
height: 68px; display: inline-block; box-shadow:0px 4px 4px rgba(0, 0, 0, 0.25);"> |
|||
<img src="assets/icons/license.png" |
|||
alt="service-icon" width="38px" |
|||
height="auto"> |
|||
</div> |
|||
</div> |
|||
<p style="margin-top: 20px; font-weight: bold;">Odoo |
|||
Licensing Consultancy</p> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="container mt-5"> |
|||
<div class="col-lg-12 d-flex flex-column justify-content-center align-items-center mt-4"> |
|||
<p class="m-0" |
|||
style="font-weight: 600; font-size: 24px; color:#000 !important"> |
|||
Our Industries</p> |
|||
|
|||
</div> |
|||
</div> |
|||
<div class="container"> |
|||
<div class="row my-5 py-4"> |
|||
<div class="col-md-3 col-sm-6 p-0"> |
|||
<div class="d-flex flex-column h-100 " |
|||
style="border-right: 1px solid rgb(209, 209, 209); border-bottom: 1px solid rgb(209, 209, 209); padding: 30px; box-shadow: 6px 0 10px rgba(228, 227, 227, 0.373);"> |
|||
<img src="assets/icons/trading-black.png" width="42px" |
|||
height="auto" alt=""> |
|||
<p style="color: #714B67;font-weight: 600; margin-top: 10px; |
|||
font-size: 1.2rem; margin-bottom: 2px;">Trading</p> |
|||
<p>Easily procure and sell your products</p> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-3 col-sm-6 p-0"> |
|||
<div class="d-flex flex-column h-100" |
|||
style="border-right: 1px solid rgb(209, 209, 209);border-bottom: 1px solid rgb(209, 209, 209); padding: 30px;"> |
|||
<img src="assets/icons/pos-black.png" width="42px" |
|||
height="auto" alt=""> |
|||
<p style="color: #714B67;font-weight: 600; margin-top: 10px; |
|||
font-size: 1.2rem; margin-bottom: 2px;">POS</p> |
|||
<p>Easy configuration and convivial experience</p> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-3 col-sm-6 p-0"> |
|||
<div class="d-flex flex-column h-100" |
|||
style="border-right: 1px solid rgb(209, 209, 209);border-bottom: 1px solid rgba(0, 0, 0, 0.2); padding: 30px; box-shadow: 0 5px 10px rgba(228, 227, 227, 0.373)"> |
|||
<img src="assets/icons/education-black.png" width="42px" |
|||
height="auto" alt=""> |
|||
<p style="color: #714B67;font-weight: 600; margin-top: 10px; |
|||
font-size: 1.2rem; margin-bottom: 2px;"> |
|||
Education</p> |
|||
<p>A platform for educational management</p> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-3 col-sm-6 p-0"> |
|||
<div class="d-flex flex-column h-100" |
|||
style="border-bottom: 1px solid rgb(209, 209, 209); padding: 30px; "> |
|||
<img src="assets/icons/manufacturing-black.png" |
|||
width="42px" height="auto" alt=""> |
|||
<p style="color: #714B67;font-weight: 600; margin-top: 10px; |
|||
font-size: 1.2rem; margin-bottom: 2px;"> |
|||
Manufacturing</p> |
|||
<p>Plan, track and schedule your operations</p> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-3 col-sm-6 p-0"> |
|||
<div class="d-flex flex-column h-100" |
|||
style="border-right: 1px solid rgb(209, 209, 209); padding: 30px;"> |
|||
<img src="assets/icons/ecom-black.png" width="42px" |
|||
height="auto" alt=""> |
|||
<p style="color: #714B67;font-weight: 600; margin-top: 10px; |
|||
font-size: 1.2rem; margin-bottom: 2px;">E-commerce & |
|||
Website</p> |
|||
<p>Mobile friendly, awe-inspiring product pages</p> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-3 col-sm-6 p-0"> |
|||
<div class="d-flex flex-column h-100" |
|||
style="border-right: 1px solid rgb(209, 209, 209); padding: 30px;box-shadow: 0 -5px 10px rgba(228, 227, 227, 0.373);"> |
|||
<img src="assets/icons/service-black.png" width="42px" |
|||
height="auto" alt=""> |
|||
<p style="color: #714B67;font-weight: 600; margin-top: 10px; |
|||
font-size: 1.2rem; margin-bottom: 2px;">Service |
|||
Management</p> |
|||
<p>Keep track of services and invoice</p> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-3 col-sm-6 p-0"> |
|||
<div class="d-flex flex-column h-100" |
|||
style="border-right: 1px solid rgb(209, 209, 209); padding: 30px; "> |
|||
<img src="assets/icons/restaurant-black.png" |
|||
width="42px" height="auto" alt=""> |
|||
<p style="color: #714B67;font-weight: 600; margin-top: 10px; |
|||
font-size: 1.2rem; margin-bottom: 2px;"> |
|||
Restaurant</p> |
|||
<p>Run your bar or restaurant methodically</p> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-3 col-sm-6 p-0"> |
|||
<div class="d-flex flex-column h-100" |
|||
style=" padding: 30px;box-shadow: -5px 0 10px rgba(228, 227, 227, 0.373);"> |
|||
<img src="assets/icons/hotel-black.png" width="42px" |
|||
height="auto" alt=""> |
|||
<p style="color: #714B67;font-weight: 600; margin-top: 10px; |
|||
font-size: 1.2rem; margin-bottom: 2px;">Hotel |
|||
Management</p> |
|||
<p>An all-inclusive hotel management application</p> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="container mt-5"> |
|||
<div class="col-lg-12 d-flex flex-column justify-content-center align-items-center mt-5"> |
|||
<p class="m-0" |
|||
style="font-weight: 600; font-size: 24px; color:#000 !important"> |
|||
Support</p> |
|||
</div> |
|||
</div> |
|||
<div class="container my-5"> |
|||
<div class="row" style="background-color: #FFFAFE;"> |
|||
<div class="col-md-6 pb-4 d-flex align-items-center justify-content-center" |
|||
style="border-right: 1px solid #D9D9D9;"> |
|||
<div style="padding: 30px;"> |
|||
<div class="d-flex align-items-center"> |
|||
<img src="assets/misc/support (1) 1.svg" alt="" |
|||
width="60px" style="margin-right: 12px;"> |
|||
<div style="padding: 0px 8px;"> |
|||
<span |
|||
style="color: #714B67;font-size: 24px;font-weight: 600;padding-bottom: 1rem;">Need |
|||
Help?</span> |
|||
<p class="m-0" style="color:#718096;">Got |
|||
questions or need help? Get in touch.</p> |
|||
<div style="font-weight: 400;"><span><img |
|||
src="assets/misc/support-email.svg" |
|||
alt="" |
|||
width="18px" |
|||
style="filter: invert(1);margin-right: 0.8rem;"></span>odoo@cybrosys.com |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="col-md-6 pb-4 d-flex align-items-center justify-content-center"> |
|||
<div style="padding: 30px;"> |
|||
<div class="d-flex align-items-center"> |
|||
<img src="assets/misc/whatsapp 1.svg" alt="" |
|||
width="60px" style="margin-right: 12px;"> |
|||
<div> |
|||
<span style="color: #714B67;font-size: 24px;font-weight: 600;">WhatsApp</span> |
|||
<p class="m-0" style="color:#718096;">Say hi to |
|||
us on WhatsApp!</p> |
|||
<div style="font-weight: 400; font-size: 16px;"><span><img |
|||
src="assets/misc/phone.svg" |
|||
alt="" width="14px" |
|||
style="filter: invert(1); margin-right: 0.8rem;"></span>+91 |
|||
99456767686 |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</section> |
|||
<!-- Optional JavaScript --> |
|||
<!-- jQuery first, then Popper.js, then Bootstrap JS --> |
|||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script> |
|||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script> |
|||
</body> |
|||
</html> |
@ -0,0 +1,56 @@ |
|||
<?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> |
|||
</header> |
|||
<sheet> |
|||
<div class="oe_button_box" name="button_box"> |
|||
<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"/> |
|||
</div> |
|||
<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> |
|||
<!-- Action for jira sprint model.--> |
|||
<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,34 @@ |
|||
<?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="//div[hasclass('oe_button_box')]" position="inside"> |
|||
<field name="sprint_active" invisible="1"/> |
|||
<button string="Sprint" class="oe_stat_button" type="object" name="action_get_sprint" icon="fa-clock-o" width="5" |
|||
height="5" 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" invisible="task_sprint_active != True" help="related sprint."/> |
|||
<field name="task_sprint_active" invisible="1"/> |
|||
</field> |
|||
</field> |
|||
</record> |
|||
</odoo> |
@ -0,0 +1,90 @@ |
|||
<?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="//form" 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" invisible="connection == False"> |
|||
<label class="col-lg-3" string="Automatic" for="automatic"/> |
|||
<field name="automatic" |
|||
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-6" 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-6" 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-6" 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-6" 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" |
|||
invisible="connection == True" type="object" class="btn-primary"/> |
|||
<div style="margin:10px !important"> |
|||
<button name="action_export_to_jira" string="Export/Sync Project and Task" |
|||
invisible="connection != True" type="object" |
|||
class="btn-primary"/> |
|||
</div> |
|||
<div style="margin:10px !important"> |
|||
<button name="action_import_from_jira" string="Import/Sync Project & Tasks" |
|||
invisible="connection == False" type="object" |
|||
class="btn-primary"/> |
|||
</div> |
|||
<div style="margin:10px !important"> |
|||
<button name="action_export_users" string="Export Users" |
|||
invisible="connection == False" type="object" |
|||
class="btn-primary"/> |
|||
</div> |
|||
<div style="margin:10px !important"> |
|||
<button name="action_import_users" string="Import Users" |
|||
invisible="connection == False" 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> |