Browse Source

APR 15: [FIX] Bug fixed 'odoo_jira_connector'

pull/331/merge
Cybrosys Technologies 2 weeks ago
parent
commit
5770d9f7d2
  1. 2
      odoo_jira_connector/__manifest__.py
  2. 5
      odoo_jira_connector/doc/RELEASE_NOTES.md
  3. 87
      odoo_jira_connector/models/mail_message.py
  4. 222
      odoo_jira_connector/models/res_config_settings.py

2
odoo_jira_connector/__manifest__.py

@ -21,7 +21,7 @@
############################################################################## ##############################################################################
{ {
'name': 'Odoo Jira Connector', 'name': 'Odoo Jira Connector',
'version': '17.0.1.0.1', 'version': '17.0.1.0.2',
'category': 'Project', 'category': 'Project',
'summary': 'Odoo Jira Connector is a valuable integration tool for ' 'summary': 'Odoo Jira Connector is a valuable integration tool for '
'businesses that use both Odoo and Jira. By connecting these ' 'businesses that use both Odoo and Jira. By connecting these '

5
odoo_jira_connector/doc/RELEASE_NOTES.md

@ -9,3 +9,8 @@
#### Version 17.0.1.0.1 #### Version 17.0.1.0.1
##### BUGFIX ##### BUGFIX
- Updated the function for importing data from Jira. - Updated the function for importing data from Jira.
#### 11.04.2025
#### Version 17.0.1.0.2
##### BUGFIX
- Fixed issue when importing comments with attachments from Jira.

87
odoo_jira_connector/models/mail_message.py

@ -21,9 +21,11 @@
############################################################################## ##############################################################################
import json import json
import requests import requests
import logging
from odoo import api, fields, models from odoo import api, fields, models
from odoo.tools import html2plaintext from odoo.tools import html2plaintext
_logger = logging.getLogger(__name__)
class MailMessage(models.Model): class MailMessage(models.Model):
""" """
@ -31,7 +33,7 @@ class MailMessage(models.Model):
override the create function override the create function
Methods: Methods:
create(values_list): create(values_list):
extends create() to create comment in Jira Extends create() to create comments in Jira, handling text and attachments
""" """
_inherit = 'mail.message' _inherit = 'mail.message'
@ -40,32 +42,56 @@ class MailMessage(models.Model):
@api.model_create_multi @api.model_create_multi
def create(self, values_list): def create(self, values_list):
""" For creating comment in Jira and comments in the chatter """ """ For creating comments in Jira and comments in the chatter """
message = super(MailMessage, self).create(values_list) messages = super(MailMessage, self).create(values_list)
if message.message_id_jira == 0:
ir_config_parameter = self.env['ir.config_parameter'].sudo() ir_config_parameter = self.env['ir.config_parameter'].sudo()
if ir_config_parameter.get_param('odoo_jira_connector.connection'): if not ir_config_parameter.get_param('odoo_jira_connector.connection'):
return messages
url = ir_config_parameter.get_param('odoo_jira_connector.url') url = ir_config_parameter.get_param('odoo_jira_connector.url')
user = ir_config_parameter.get_param( user = ir_config_parameter.get_param('odoo_jira_connector.user_id_jira')
'odoo_jira_connector.user_id_jira') password = ir_config_parameter.get_param('odoo_jira_connector.api_token')
password = ir_config_parameter.get_param( headers = {
'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', 'Accept': 'application/json',
'Content-Type': 'application/json'}, 'Content-Type': 'application/json'
auth=(user, password)) }
for message in messages:
if message.message_id_jira == 0 and message.model == 'project.task':
task = self.env['project.task'].browse(message.res_id)
if not task.task_id_jira:
continue
# Extract text content from the message body
current_message = str(html2plaintext(message.body)).strip()
comment_url = f'{url}rest/api/3/issue/{task.task_id_jira}/comment'
# Fetch existing Jira comments to avoid duplicates
try:
response = requests.get(comment_url, headers=headers, auth=(user, password))
response.raise_for_status()
data = response.json() data = response.json()
if response.status_code == 200: except requests.RequestException as e:
list_of_comments_jira = [ _logger.error(f"Failed to fetch Jira comments for task {task.task_id_jira}: {str(e)}")
str(comments['body']['content'][0]['content'][0][ continue
'text']) for comments in data['comments']]
if current_message not in list( # Parse Jira comments to extract text content
filter(None, list_of_comments_jira)): list_of_comments_jira = []
for comment in data.get('comments', []):
text_content = ""
comment_body = comment.get('body', {}).get('content', [])
for content_block in comment_body:
if content_block.get('type') == 'paragraph':
for content_item in content_block.get('content', []):
if content_item.get('type') == 'text':
text_content += content_item.get('text', '') + " "
elif content_item.get('type') == 'emoji':
text_content += content_item.get('attrs', {}).get('text', '') + " "
if text_content.strip():
list_of_comments_jira.append(text_content.strip())
# Post comment to Jira if it's new
if current_message and current_message not in list_of_comments_jira:
data = json.dumps({ data = json.dumps({
'body': { 'body': {
'type': 'doc', 'type': 'doc',
@ -79,12 +105,13 @@ class MailMessage(models.Model):
}] }]
} }
}) })
response = requests.post( try:
url + 'rest/api/3/issue/%s/comment' % ( response = requests.post(comment_url, headers=headers, data=data,
task.task_id_jira), headers={ auth=(user, password))
'Accept': 'application/json', response.raise_for_status()
'Content-Type': 'application/json'},
data=data, auth=(user, password))
data = response.json() data = response.json()
message.write({'message_id_jira': data.get('id')}) message.write({'message_id_jira': data.get('id')})
return message except requests.RequestException as e:
_logger.error(f"Failed to post comment to Jira for task {task.task_id_jira}: {str(e)}")
return messages

222
odoo_jira_connector/models/res_config_settings.py

@ -26,9 +26,12 @@ import re
import requests import requests
from requests.auth import HTTPBasicAuth from requests.auth import HTTPBasicAuth
from odoo import fields, models, _ from odoo import fields, models, _
import logging
from odoo.exceptions import ValidationError from odoo.exceptions import ValidationError
from odoo.tools import html2plaintext from odoo.tools import html2plaintext
_logger = logging.getLogger(__name__)
# The Header parameters # The Header parameters
HEADERS = { HEADERS = {
'Accept': 'application/json', 'Accept': 'application/json',
@ -141,7 +144,7 @@ class ResConfigSettings(models.TransientModel):
} }
} }
} }
except Exception: except Exception as e:
raise ValidationError(_('Please Enter Valid Credentials.')) raise ValidationError(_('Please Enter Valid Credentials.'))
def action_export_to_jira(self): def action_export_to_jira(self):
@ -461,102 +464,171 @@ class ResConfigSettings(models.TransientModel):
return '' return ''
def action_import_from_jira(self): def action_import_from_jira(self):
""" Import all the projects and corresponding tasks """ Import projects, tasks, comments, and attachments from Jira to Odoo.
from Odoo to Jira. If a project or task is modified in Odoo, Updates existing tasks if modified in Jira. Handles text, emojis, and image attachments without duplication.
it will also be updated in Jira.
""" """
auth = HTTPBasicAuth(self.user_id_jira, self.api_token) auth = HTTPBasicAuth(self.user_id_jira, self.api_token)
response = requests.get(self.url + 'rest/api/2/project',
# Fetch Jira projects
response = requests.get(f"{self.url}rest/api/2/project",
headers=JIRA_HEADERS, auth=auth) headers=JIRA_HEADERS, auth=auth)
projects = json.loads(response.text) response.raise_for_status()
odoo_projects = self.env['project.project'].search([]) projects = response.json()
jira_project_ids = [int(a_dict['id']) for a_dict in projects] odoo_projects = self.env['project.project'].search([])
name_list = [a_dict['name'] for a_dict in projects] odoo_project_map = {p.project_id_jira: p.id for p in odoo_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): for project_data in projects:
if jira_id in [project.project_id_jira for project in jira_id = int(project_data['id'])
odoo_projects]: name = project_data['name']
response = requests.get(self.url + 'rest/api/3/search', key = project_data['key']
headers=JIRA_HEADERS,
params={'jql': 'project = %s' % key},
auth=auth)
data = response.json()
project = self.env['project.project'].search(
[('project_id_jira', '=', jira_id)], limit=1)
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'])], limit=1)
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, # Create or get Odoo project
auth=auth) if jira_id in odoo_project_map:
data = response.json() project_id = odoo_project_map[jira_id]
messages = self.env['mail.message'].search( project = self.env['project.project'].browse(project_id)
[('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: else:
project = self.env['project.project'].create({ project = self.env['project.project'].create({
'name': name, 'name': name,
'project_id_jira': jira_id, 'project_id_jira': jira_id,
'jira_project_key': key 'jira_project_key': key
}) })
self.import_project_count = 10 self.import_project_count += 1
response = requests.get(self.url + 'rest/api/3/search', odoo_project_map[jira_id] = project.id
# Fetch issues for the project
response = requests.get(
f"{self.url}rest/api/3/search",
headers=JIRA_HEADERS, headers=JIRA_HEADERS,
params={'jql': 'project = %s' % key}, params={'jql': f'project = {key}'},
auth=auth) auth=auth
data = response.json() )
response.raise_for_status()
issues = response.json().get('issues', [])
odoo_tasks = self.env['project.task'].search(
[('project_id', '=', project.id)])
task_jira_ids = {task.task_id_jira: task.id for task in odoo_tasks}
for issue in issues:
issue_key = issue['key']
for issue in data['issues']: # Create or get Odoo task
comment_url = self.url + 'rest/api/3/issue/%s/comment' % \ if issue_key in task_jira_ids:
issue['key'] task = self.env['project.task'].browse(
task_jira_ids[issue_key])
else:
task = self.env['project.task'].create({ task = self.env['project.task'].create({
'project_id': project.id, 'project_id': project.id,
'name': issue['fields']['summary'], 'name': issue['fields']['summary'],
'task_id_jira': issue['key'] 'task_id_jira': issue_key
}) })
self.import_task_count += 1 self.import_task_count += 1
# Fetch issue attachments
attachment_url = f"{self.url}rest/api/3/issue/{issue_key}?fields=attachment"
try:
attachment_response = requests.get(attachment_url,
headers=JIRA_HEADERS,
auth=auth)
attachment_response.raise_for_status()
issue_attachments = {att['filename']: att for att in
attachment_response.json().get(
'fields', {}).get('attachment',
[])}
except requests.RequestException as e:
_logger.error(
f"Failed to fetch attachments for issue {issue_key}: {str(e)}")
issue_attachments = {}
# Fetch comments
comment_url = f"{self.url}rest/api/3/issue/{issue_key}/comment"
response = requests.get(comment_url, headers=JIRA_HEADERS, response = requests.get(comment_url, headers=JIRA_HEADERS,
auth=auth) auth=auth)
data = response.json() response.raise_for_status()
comments = response.json().get('comments', [])
# Get existing Odoo comments and attachments to avoid duplicates
messages = self.env['mail.message'].search( messages = self.env['mail.message'].search(
[('res_id', '=', task.id), [('res_id', '=', task.id), ('model', '=', 'project.task')])
('model', '=', 'project.task')]) odoo_comment_ids = {
odoo_comment_list = [str(html2plaintext(chat.body)) for chat m.message_id_jira: html2plaintext(m.body).strip() for m in
in messages] messages if m.message_id_jira}
jira_comment_list = [ existing_attachments = self.env['ir.attachment'].search([
str(comment['body']['content'][0]['content'][0]['text']) ('res_model', '=', 'project.task'),
for comment in data['comments'] if str( ('res_id', '=', task.id)
comment['body']['content'][0]['content'][0][ ])
'text']) not in odoo_comment_list] existing_attachment_names = {att.name for att in
comment_list = list(filter(None, jira_comment_list)) existing_attachments}
for comment in comment_list: for comment in comments:
task.message_post(body=comment) jira_comment_id = int(comment['id'])
text_content = ""
attachments = []
comment_body = comment.get('body', {}).get('content', [])
# Process comment content
for block in comment_body:
if block.get('type') == 'paragraph':
for item in block.get('content', []):
if item.get('type') == 'text':
text_content += item.get('text', '') + " "
elif item.get('type') == 'emoji':
text_content += item.get('attrs', {}).get(
'text', '') + " "
elif block.get('type') == 'mediaSingle':
for item in block.get('content', []):
if item.get('type') == 'media':
media_attrs = item.get('attrs', {})
media_id = media_attrs.get('id')
filename = media_attrs.get('alt',
f"attachment-{media_id}")
if filename in issue_attachments:
attachment = issue_attachments[filename]
if attachment[
'filename'] not in existing_attachment_names:
attachments.append({
'url': attachment['content'],
'filename': attachment[
'filename'],
'media_id': media_id
})
else:
_logger.warning(
f"Attachment '{filename}' (media_id: {media_id}) not found for issue {issue_key}")
# Import text comment if not already in Odoo
text_content = text_content.strip()
if text_content and jira_comment_id not in odoo_comment_ids:
message = task.message_post(body=text_content)
message.write({'message_id_jira': jira_comment_id})
odoo_comment_ids[jira_comment_id] = text_content
# Import attachments
for attachment in attachments:
try:
response = requests.get(attachment['url'],
headers=JIRA_HEADERS,
auth=auth, stream=True)
response.raise_for_status()
attachment_data = base64.b64encode(
response.content).decode('utf-8')
self.env['ir.attachment'].create({
'name': attachment['filename'],
'datas': attachment_data,
'res_model': 'project.task',
'res_id': task.id,
'mimetype': self._guess_mimetype(
attachment['filename']),
})
existing_attachment_names.add(
attachment['filename'])
except requests.RequestException as e:
_logger.error(
f"Failed to download attachment '{attachment['filename']}' (media_id: {attachment['media_id']}, URL: {attachment['url']}) for task {task.task_id_jira}: {str(e)}")
def _guess_mimetype(self, filename):
""" Guess the MIME type based on the file extension """
from mimetypes import guess_type
mime_type, _ = guess_type(filename)
return mime_type or 'application/octet-stream'

Loading…
Cancel
Save