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. 125
      odoo_jira_connector/models/mail_message.py
  4. 230
      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.

125
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,60 +33,85 @@ 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'
message_id_jira = fields.Integer(string='Message ID', message_id_jira = fields.Integer(string='Message ID',
help='ID for the comments in Jira.') help='ID for the comments in Jira.')
@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 not ir_config_parameter.get_param('odoo_jira_connector.connection'):
if ir_config_parameter.get_param('odoo_jira_connector.connection'): return messages
url = ir_config_parameter.get_param('odoo_jira_connector.url')
user = ir_config_parameter.get_param( url = ir_config_parameter.get_param('odoo_jira_connector.url')
'odoo_jira_connector.user_id_jira') user = ir_config_parameter.get_param('odoo_jira_connector.user_id_jira')
password = ir_config_parameter.get_param( password = ir_config_parameter.get_param('odoo_jira_connector.api_token')
'odoo_jira_connector.api_token') headers = {
if message.model == 'project.task': 'Accept': 'application/json',
task = self.env['project.task'].browse(message.res_id) 'Content-Type': 'application/json'
current_message = str(html2plaintext(message.body)) }
response = requests.get(
f'{url}rest/api/3/issue/{task.task_id_jira}/comment', for message in messages:
headers={ if message.message_id_jira == 0 and message.model == 'project.task':
'Accept': 'application/json', task = self.env['project.task'].browse(message.res_id)
'Content-Type': 'application/json'}, if not task.task_id_jira:
auth=(user, password)) 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 = []
data = json.dumps({ for comment in data.get('comments', []):
'body': { text_content = ""
'type': 'doc', comment_body = comment.get('body', {}).get('content', [])
'version': 1, for content_block in comment_body:
'content': [{ if content_block.get('type') == 'paragraph':
'type': 'paragraph', for content_item in content_block.get('content', []):
'content': [{ if content_item.get('type') == 'text':
'text': current_message, text_content += content_item.get('text', '') + " "
'type': '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())
})
response = requests.post( # Post comment to Jira if it's new
url + 'rest/api/3/issue/%s/comment' % ( if current_message and current_message not in list_of_comments_jira:
task.task_id_jira), headers={ data = json.dumps({
'Accept': 'application/json', 'body': {
'Content-Type': 'application/json'}, 'type': 'doc',
data=data, auth=(user, password)) 'version': 1,
data = response.json() 'content': [{
message.write({'message_id_jira': data.get('id')}) 'type': 'paragraph',
return message 'content': [{
'text': current_message,
'type': 'text'
}]
}]
}
})
try:
response = requests.post(comment_url, headers=headers, data=data,
auth=(user, password))
response.raise_for_status()
data = response.json()
message.write({'message_id_jira': data.get('id')})
except requests.RequestException as e:
_logger.error(f"Failed to post comment to Jira for task {task.task_id_jira}: {str(e)}")
return messages

230
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()
projects = response.json()
odoo_projects = self.env['project.project'].search([]) odoo_projects = self.env['project.project'].search([])
odoo_project_map = {p.project_id_jira: p.id for p in odoo_projects}
jira_project_ids = [int(a_dict['id']) for a_dict in projects] for project_data in projects:
name_list = [a_dict['name'] for a_dict in projects] jira_id = int(project_data['id'])
key_list = [a_dict['key'] for a_dict in projects] name = project_data['name']
key = project_data['key']
for (name, key, jira_id) in zip(name_list, key_list, jira_project_ids): # Create or get Odoo project
if jira_id in [project.project_id_jira for project in if jira_id in odoo_project_map:
odoo_projects]: project_id = odoo_project_map[jira_id]
response = requests.get(self.url + 'rest/api/3/search', project = self.env['project.project'].browse(project_id)
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,
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: 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
headers=JIRA_HEADERS,
params={'jql': 'project = %s' % key}, # Fetch issues for the project
auth=auth) response = requests.get(
data = response.json() f"{self.url}rest/api/3/search",
headers=JIRA_HEADERS,
params={'jql': f'project = {key}'},
auth=auth
)
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
response = requests.get(comment_url, headers=JIRA_HEADERS, # Fetch issue attachments
auth=auth) attachment_url = f"{self.url}rest/api/3/issue/{issue_key}?fields=attachment"
data = response.json() try:
messages = self.env['mail.message'].search( attachment_response = requests.get(attachment_url,
[('res_id', '=', task.id), headers=JIRA_HEADERS,
('model', '=', 'project.task')]) auth=auth)
odoo_comment_list = [str(html2plaintext(chat.body)) for chat attachment_response.raise_for_status()
in messages] issue_attachments = {att['filename']: att for att in
jira_comment_list = [ attachment_response.json().get(
str(comment['body']['content'][0]['content'][0]['text']) 'fields', {}).get('attachment',
for comment in data['comments'] if str( [])}
comment['body']['content'][0]['content'][0][ except requests.RequestException as e:
'text']) not in odoo_comment_list] _logger.error(
comment_list = list(filter(None, jira_comment_list)) f"Failed to fetch attachments for issue {issue_key}: {str(e)}")
issue_attachments = {}
for comment in comment_list: # Fetch comments
task.message_post(body=comment) comment_url = f"{self.url}rest/api/3/issue/{issue_key}/comment"
response = requests.get(comment_url, headers=JIRA_HEADERS,
auth=auth)
response.raise_for_status()
comments = response.json().get('comments', [])
# Get existing Odoo comments and attachments to avoid duplicates
messages = self.env['mail.message'].search(
[('res_id', '=', task.id), ('model', '=', 'project.task')])
odoo_comment_ids = {
m.message_id_jira: html2plaintext(m.body).strip() for m in
messages if m.message_id_jira}
existing_attachments = self.env['ir.attachment'].search([
('res_model', '=', 'project.task'),
('res_id', '=', task.id)
])
existing_attachment_names = {att.name for att in
existing_attachments}
for comment in comments:
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