You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

670 lines
31 KiB

# -*- coding: utf-8 -*-
################################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>)
# Author: Rosmy John (odoo@cybrosys.com)
#
# You can modify it under the terms of the GNU LESSER
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details.
#
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE
# (LGPL v3) along with this program.
# If not, see <http://www.gnu.org/licenses/>.
#
################################################################################
import base64
import json
from datetime import datetime
import requests
from requests.auth import HTTPBasicAuth
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
from odoo.tools import html2plaintext
# The Header parameters
HEADERS = {'Accept': 'application/json', 'Content-Type': 'application/json'}
class ProjectProject(models.Model):
"""
This class is inherited for adding some extra field and override the
create and write function also to add function to show sprint
Methods:
create(vals_list):
extends create() to export project to Jira
write(vals):
extends write() to update corresponding project in Jira
"""
_inherit = 'project.project'
project_id_jira = fields.Integer(string='Jira Project ID',
help='Corresponding project id of Jira.',
readonly=True)
jira_project_key = fields.Char(string='Jira Project Key',
help='Corresponding project key of Jira.',
readonly=True)
sprint_active = fields.Boolean(string='Sprint active',
help='To show sprint smart button.',default=True)
board_id_jira = fields.Integer(string='Jira Board ID',
help='Corresponding Board id of Jira.',
readonly=True)
def action_get_sprint(self):
"""Getting sprint inside the project"""
return {
'type': 'ir.actions.act_window',
'name': 'Sprints',
'view_mode': 'tree,form',
'res_model': 'jira.sprint',
'context': {'default_project_id': self.id},
'domain': [('project_id', '=', self.id)],
}
@api.model_create_multi
def create(self, vals_list):
""" Overrides create method of project to export project to Jira """
self = self.with_context(mail_create_nosubscribe=True)
projects = super().create(vals_list)
jira_connection = self.env['ir.config_parameter'].sudo().get_param(
'odoo_jira_connector.connection')
if jira_connection:
jira_url = self.env['ir.config_parameter'].sudo().get_param(
'odoo_jira_connector.url', False)
user = self.env['ir.config_parameter'].sudo().get_param(
'odoo_jira_connector.user_id_jira', False)
password = self.env['ir.config_parameter'].sudo().get_param(
'odoo_jira_connector.api_token', False)
auth = HTTPBasicAuth(user, password)
project_headers = {'Accept': 'application/json'}
response = requests.request(
'GET', jira_url + '/rest/api/3/project/',
headers=project_headers, auth=auth)
projects_json = json.dumps(
json.loads(response.text), sort_keys=True, indent=4,
separators=(',', ': '))
project_json = json.loads(projects_json)
name_list = [project['name'] for project in project_json]
key = projects.name.upper()
project_key = key[:3] + '1' + key[-3:]
project_keys = project_key.replace(' ', '')
auth = HTTPBasicAuth(user, password)
project_payload = {
'name': projects.name, 'key': project_keys,
'templateKey': 'com.pyxis.greenhopper.jira:gh-simplified'
'-kanban-classic'
}
if projects.name not in name_list:
response = requests.request(
'POST', jira_url + 'rest/simplified/latest/project',
data=json.dumps(project_payload),
headers=HEADERS, auth=auth)
data = response.json()
if 'projectId' in data:
projects.write({'project_id_jira': data['projectId'],
'jira_project_key': data['projectKey']})
self.env['ir.config_parameter'].sudo().set_param(
'import_project_count', int(
self.env['ir.config_parameter'].sudo().get_param(
'import_project_count')) + 1)
elif 'errors' in data and 'projectName' in data['errors']:
raise ValidationError(
"A project with this name already exists. Please "
"rename the project.")
elif 'errors' in data and 'projectKey' in data['errors']:
raise ValidationError(data['errors']['projectKey'])
return projects
def write(self, vals):
""" Overrides the write method of project.project to update project
name in Jira when we update the project in Odoo"""
jira_connection = self.env['ir.config_parameter'].sudo().get_param(
'odoo_jira_connector.connection')
if jira_connection:
for project in self:
jira_url = self.env['ir.config_parameter'].sudo().get_param(
'odoo_jira_connector.url')
user = self.env['ir.config_parameter'].sudo().get_param(
'odoo_jira_connector.user_id_jira')
password = self.env['ir.config_parameter'].sudo().get_param(
'odoo_jira_connector.api_token')
auth = (user, password)
headers = {
"Accept": "application/json",
"Content-Type": "application/json"
}
url = (f"{jira_url}/rest/api/3/project/"
f"{project.jira_project_key}")
payload = json.dumps({
"name": vals.get('name'),
})
payload_json = json.loads(payload)
response = requests.get(
url,
headers=headers,
auth=auth)
data = response.json()
if 'name' in data:
if data['name'] != payload_json['name']:
requests.request(
"PUT",
url, data=payload, headers=headers, auth=auth)
else:
requests.request(
"PUT",
url, data=payload, headers=headers, auth=auth)
return super(ProjectProject, self).write(vals)
class ProjectTask(models.Model):
"""
This class is inherited for adding some extra field and override the
create function
Methods:
create(vals_list):
extends create() to export tasks to Jira
unlink():
extends unlink() to delete a task in Jira when we delete the
task in Odoo
write(vals):
extends write() to update a task in Jira when we update the
task in Odoo
"""
_inherit = 'project.task'
task_id_jira = fields.Char(string='Jira Task ID', help='Task id of Jira.',
readonly=True)
sprint_id = fields.Many2one('jira.sprint',
help="Sprint of this task.", readonly=True)
task_sprint_active = fields.Boolean(string="Active Sprint",
compute="_compute_task_sprint_active",
store=True,
help="Boolean field to check whether "
"the sprint is active or not.")
@api.depends('project_id.sprint_active')
def _compute_task_sprint_active(self):
"""compute function to make sprint_id invisible by changing
'task_sprint_active' field to true"""
for rec in self:
if rec.project_id.sprint_active:
rec.task_sprint_active = True
@api.model
def create(self, vals_list):
""" Override the create method of tasks to export tasks to Jira """
res = super(ProjectTask, self).create(vals_list)
jira_connection = self.env['ir.config_parameter'].sudo().get_param(
'odoo_jira_connector.connection')
if jira_connection:
jira_url = self.env['ir.config_parameter'].sudo().get_param(
'odoo_jira_connector.url')
user = self.env['ir.config_parameter'].sudo().get_param(
'odoo_jira_connector.user_id_jira')
password = self.env['ir.config_parameter'].sudo().get_param(
'odoo_jira_connector.api_token')
query = {'jql': 'project = %s' % res.project_id.jira_project_key}
requests.get(jira_url + 'rest/api/3/search', headers=HEADERS,
params=query, auth=(user, password))
if not res.task_id_jira:
payload = json.dumps({
'fields': {
'project': {'key': res.project_id.jira_project_key},
'summary': res.name,
'description': html2plaintext(res.description),
'issuetype': {'name': 'Task'}
}
})
response = requests.post(
jira_url + '/rest/api/2/issue', headers=HEADERS,
data=payload, auth=(user, password))
data = response.json()
res.task_id_jira = str(data.get('key'))
self.env['ir.config_parameter'].sudo().set_param(
'export_task_count', int(
self.env['ir.config_parameter'].sudo().get_param(
'export_task_count')) + 1)
return res
def unlink(self):
""" Overrides the unlink method of task to delete a task in Jira when
we delete the task in Odoo """
for task in self:
if task.stage_id and task.stage_id.fold:
raise Warning(_('You cannot delete a task in a folded stage.'))
jira_connection = self.env['ir.config_parameter'].sudo().get_param(
'odoo_jira_connector.connection')
if jira_connection:
jira_url = self.env['ir.config_parameter'].sudo().get_param(
'odoo_jira_connector.url', '')
user = self.env['ir.config_parameter'].sudo().get_param(
'odoo_jira_connector.user_id_jira')
password = self.env['ir.config_parameter'].sudo().get_param(
'odoo_jira_connector.api_token')
if task.task_id_jira:
requests.delete(
jira_url + '/rest/api/3/issue/' + task.task_id_jira,
headers=HEADERS, auth=(user, password))
return super(ProjectTask, self).unlink()
def write(self, vals):
""" Overrides the write method of task to update a task's name in
Jira when we update the task in Odoo"""
jira_connection = self.env['ir.config_parameter'].sudo().get_param(
'odoo_jira_connector.connection')
if jira_connection:
jira_url = self.env['ir.config_parameter'].sudo().get_param(
'odoo_jira_connector.url', '')
user = self.env['ir.config_parameter'].sudo().get_param(
'odoo_jira_connector.user_id_jira')
password = self.env['ir.config_parameter'].sudo().get_param(
'odoo_jira_connector.api_token')
for task in self:
if task.task_id_jira and 'name' in vals:
new_task_name = vals['name']
payload = {
"fields": {
"summary": new_task_name
}
}
requests.put(
jira_url + '/rest/api/3/issue/' + task.task_id_jira,
json=payload, headers=HEADERS, auth=(user, password))
return super(ProjectTask, self).write(vals)
def webhook_data_handle(self, jira_data, webhook_event):
"""Function to Handle Jira Data Received from Webhook"""
if webhook_event == 'project_created':
self.create_project(jira_data)
elif webhook_event == 'project_updated':
self.update_project(jira_data)
elif webhook_event == 'project_soft_deleted':
self.delete_project(jira_data)
elif webhook_event == 'jira:issue_created':
self.create_task(jira_data)
elif webhook_event == 'jira:issue_deleted':
self.delete_task(jira_data)
elif webhook_event == 'comment_created':
self.create_comment(jira_data)
elif webhook_event == 'comment_deleted':
self.delete_comment(jira_data)
elif webhook_event == 'user_created':
self.create_user(jira_data)
elif webhook_event == 'user_deleted':
self.delete_user(jira_data)
elif webhook_event == 'board_configuration_changed':
self.board_configuration_change(jira_data)
elif webhook_event == 'jira:issue_updated':
self.update_task(jira_data)
elif webhook_event == 'attachment_deleted':
self.delete_attachment(jira_data)
elif webhook_event == 'sprint_started':
self.sprint_started(jira_data)
elif webhook_event == 'sprint_closed':
self.sprint_closed(jira_data)
def create_project(self, jira_data):
"""function to create project based on webhook response"""
jira_project = jira_data['project']
existing_project = self.env['project.project'].sudo().search(
[('project_id_jira', '=', jira_project['id'])])
values = {
'name': jira_project['name'],
'project_id_jira': jira_project['id'],
'jira_project_key': jira_project['key']
}
if not existing_project:
imported_project = self.env['project.project'].sudo().create(
values)
url = self.env['ir.config_parameter'].sudo().get_param(
'odoo_jira_connector.url')
user = self.env['ir.config_parameter'].sudo().get_param(
'odoo_jira_connector.user_id_jira')
password = self.env['ir.config_parameter'].sudo().get_param(
'odoo_jira_connector.api_token')
auth = HTTPBasicAuth(user, password)
headers = {
"Accept": "application/json"
}
response = requests.request(
"GET",
url + "/rest/api/3/project/" + jira_project['key'],
headers=headers,
auth=auth
)
data = response.json()
style_value = data.get('style')
if style_value == 'classic':
imported_project.write({'sprint_active': False})
else:
imported_project.write({'sprint_active': True})
def update_project(self, jira_data):
"""function to update project based on webhook response"""
project_id = jira_data['project']['id']
existing_project = self.env['project.project'].sudo().search(
[('project_id_jira', '=', project_id)])
if existing_project.name != jira_data['project']['name']:
existing_project.write({'name': jira_data['project']['name']})
def delete_project(self, jira_data):
"""function to delete project based on webhook response"""
project_id = (jira_data['project']['id'])
self.env['project.project'].sudo().search(
[('project_id_jira', '=', project_id)]).unlink()
def create_task(self, jira_data):
"""function to create task based on webhook response"""
task_name = jira_data['issue']['fields']['summary']
task_key = jira_data['issue']['key']
jira_project_id = jira_data['issue']['fields']['project']['id']
project = self.env['project.project'].sudo().search(
[('project_id_jira', '=', int(jira_project_id))])
existing_task = self.env['project.task'].sudo().search(
[('task_id_jira', '=', jira_data['issue']['key'])])
if not existing_task:
self.env['project.task'].sudo().create({
'project_id': project.id,
'name': task_name,
'task_id_jira': task_key
})
def delete_task(self, jira_data):
"""function to delete task based on webhook response"""
task_key = jira_data['issue']['key']
self.env['project.task'].sudo().search(
[('task_id_jira', '=', task_key)]).unlink()
def create_comment(self, jira_data):
"""function to create comment based on webhook response"""
text = jira_data['comment']['body']
task_key = jira_data['issue']['key']
task = self.env['project.task'].sudo().search(
[('task_id_jira', '=', task_key)])
existing_message = self.env['mail.message'].sudo().search(
['&', ('res_id', '=', task.id),
('model', '=', 'project.task'),
('message_id_jira', '=', jira_data['comment']['id'])])
if not existing_message:
input_string = str(text)
parts = input_string.split(".")
if len(parts) > 1:
body = parts[1]
else:
body = parts[0]
self.env['mail.message'].sudo().create(
{"body": html2plaintext(body),
'model': 'project.task',
'res_id': task.id,
'message_id_jira': jira_data['comment']['id']
})
def delete_comment(self, jira_data):
"""function to delete comment based on webhook response"""
self.env['mail.message'].sudo().search(
[('message_id_jira', '=',
jira_data['comment']['id'])]).unlink()
def create_user(self, jira_data):
"""function to create user based on webhook response"""
existing_user = self.env['res.user']. \
search([('jira_user_key', '=', jira_data['user']['accountId'])])
if not existing_user:
self.env['res.users'].sudo().create({
'login': jira_data['user']['displayName'],
'name': jira_data['user']['displayName'],
'jira_user_key': jira_data['user']['accountId']
})
def delete_user(self, jira_data):
"""function to delete user based on webhook response"""
self.env['res.users'].sudo().search(
[('jira_user_key', '=', jira_data['accountId'])]).unlink()
def board_configuration_change(self, jira_data):
"""function to create stages or write project into stages based on
webhook response"""
columns = jira_data['configuration']['columnConfig']['columns']
if jira_data['configuration'].get('location'):
project_key = jira_data['configuration']['location']['key']
project = self.env['project.project'].sudo().search(
[('jira_project_key', '=', project_key)])
sequence_value = 1
for column in columns:
if column['name'] != 'Backlog':
stages_jira_id = column['statuses'][0]['id']
existing_stage = self.env[
'project.task.type'].sudo().search(
[('stages_jira_id', '=', stages_jira_id)])
existing_stage.write({'project_ids': project,
'sequence': sequence_value})
if not existing_stage:
values = {
'name': column['name'],
'stages_jira_id': stages_jira_id,
'jira_project_key': project_key,
'project_ids': project,
'sequence': sequence_value,
}
self.env['project.task.type'].sudo().create(
values)
sequence_value += 1
project.write({'board_id_jira': jira_data['configuration']['id']})
else:
board_id_jira = jira_data['configuration']['id']
project = self.env['project.project'].search(
[('board_id_jira', '=', board_id_jira)])
existing_stages = self.env[
'project.task.type'].sudo().search(
[('project_ids', 'in', project.id),
('stages_jira_id', '!=', '0')])
jira_status_ids = []
for column in columns:
for status in column['statuses']:
jira_status_ids.append(status['id'])
if len(jira_status_ids) < len(existing_stages.ids):
removed_stage = self.env[
'project.task.type'].sudo().search(
[('project_ids', 'in', project.id),
('stages_jira_id', 'not in', jira_status_ids)])
removed_stage.unlink()
elif len(jira_status_ids) > len(existing_stages.ids):
columns = jira_data['configuration']['columnConfig'][
'columns']
num_stages = len(columns)
stage_id = columns[num_stages - 1]['statuses'][0]['id']
values = {
'name': columns[num_stages - 1]['name'],
'stages_jira_id': stage_id,
'project_ids': project,
'sequence': num_stages,
}
self.env['project.task.type'].sudo().create(values)
def update_task(self, jira_data):
"""function to update a task, which includes changing the task stage,
adding attachments, adding a description to the task,
changing the task's name,
and adding a sprint based on webhook response"""
task_key = jira_data['issue']['key']
imported_task = self.env['project.task'].sudo().search(
[('task_id_jira', '=', task_key)])
to_value = jira_data['changelog']['items'][0]['to']
if jira_data['changelog']['items'][0]['field'] == 'resolution':
second_to_value = jira_data['changelog']['items'][1]['to']
task_stage = self.env['project.task.type'].sudo().search(
[('stages_jira_id', '=', second_to_value)])
imported_task.write({'stage_id': task_stage.id})
elif jira_data['changelog']['items'][0]['field'] == 'status':
task_stage = self.env['project.task.type'].sudo().search(
[('stages_jira_id', '=', to_value)])
imported_task.write({'stage_id': task_stage.id})
elif jira_data['changelog']['items'][0]['field'] == 'Attachment':
if jira_data['changelog']['items'][0]['to'] != 'None':
attachments = jira_data["issue"]['fields']['attachment']
jira_attachment_id = [attachment['id'] for attachment in
attachments]
num_attachments = len(jira_attachment_id)
user_name = self.env[
'ir.config_parameter'].sudo().get_param(
'odoo_jira_connector.user_id_jira')
api_token = self.env[
'ir.config_parameter'].sudo().get_param(
'odoo_jira_connector.api_token')
auth = HTTPBasicAuth(user_name, api_token)
if num_attachments > 0:
name = attachments[num_attachments - 1].get('filename')
mime_type = attachments[num_attachments - 1].get(
'mimeType')
src = attachments[num_attachments - 1].get('content')
jira_id = attachments[num_attachments - 1].get('id')
image = base64.b64encode(
requests.get(src, auth=auth).content)
existing_attachments = self.env[
'ir.attachment'].sudo().search(
[('res_id', '=', imported_task.id),
('res_model', '=', 'project.task'),
('attachment_id_jira', '=', jira_id)]
)
values = {
'name': name,
'type': 'binary',
'datas': image,
'res_model': 'project.task',
'res_id': imported_task.id,
'mimetype': mime_type,
'attachment_id_jira': jira_id
}
if not existing_attachments:
self.env['ir.attachment'].sudo().create(values)
else:
pass
elif jira_data['changelog']['items'][0]['field'] == 'description':
imported_task.update({'description': jira_data['changelog']
['items'][0]['toString']})
elif jira_data['changelog']['items'][0]['field'] == 'summary':
if imported_task.name != jira_data['changelog']['items'][0] \
['toString']:
imported_task.write(
{'name': jira_data['changelog']['items'][0]
['toString']})
elif jira_data['changelog']['items'][0]['field'] == 'Sprint':
project_key = jira_data['issue']['fields']['project']['key']
project = self.env['project.project'].sudo().search(
[('jira_project_key', '=', project_key)])
custom_field = jira_data['issue']['fields']['customfield_10020']
if len(custom_field) > 1:
jira_sprint = self.env['jira.sprint'].sudo().search(
[('sprint_id_jira', '=',
custom_field[len(custom_field) - 1]['id'])])
if not jira_sprint:
vals = {
'name': custom_field[len(custom_field) - 1]['name'],
'sprint_id_jira':
custom_field[len(custom_field) - 1]['id'],
'project_id': project.id
}
sprint = self.env['jira.sprint'].sudo().create(vals)
if project.task_ids:
for rec in project.task_ids:
rec.write({'sprint_id': sprint.id})
else:
jira_sprint = self.env['jira.sprint'].sudo().search([(
'sprint_id_jira', '=', custom_field[0]['id'])])
if not jira_sprint:
vals = {
'name': custom_field[0]['name'],
'sprint_id_jira': custom_field[0]['id'],
'project_id': project.id
}
sprint = self.env['jira.sprint'].sudo().create(vals)
if project.task_ids:
for rec in project.task_ids:
rec.write({'sprint_id': sprint.id})
if rec.task_id_jira != task_key:
self.create({
'project_id': project.id,
'name': jira_data['issue']['fields'][
'summary'],
'task_id_jira': task_key,
'sprint_id': jira_sprint.id
})
break
else:
task_name = jira_data['issue']['fields']['summary']
self.create({
'project_id': project.id,
'name': task_name,
'task_id_jira': task_key,
'sprint_id': sprint.id
})
else:
if project.task_ids:
for rec in project.task_ids:
rec.write({'sprint_id': jira_sprint.id})
if rec.task_id_jira != task_key:
self.create({
'project_id': project.id,
'name': jira_data['issue']['fields'][
'summary'],
'task_id_jira': task_key,
'sprint_id': jira_sprint.id
})
break
else:
task_name = jira_data['issue']['fields']['summary']
self.create({
'project_id': project.id,
'name': task_name,
'task_id_jira': task_key,
'sprint_id': jira_sprint.id
})
def delete_attachment(self, jira_data):
"""function to delete attachment based on the response received from
webhook"""
jira_id = jira_data['attachment']['id']
self.env['ir.attachment'].sudo().search(
[('attachment_id_jira', '=', jira_id)]).unlink()
def sprint_started(self, jira_data):
"""function to start sprint which is created using webhook response"""
sprint_in_odoo = self.env['jira.sprint'].sudo().search(
[('sprint_id_jira', '=', jira_data['sprint']['id'])])
if sprint_in_odoo:
start_date = jira_data['sprint']['startDate']
input_start_date = datetime. \
strptime(start_date, '%Y-%m-%dT%H:%M:%S.%fZ')
jira_start_date = input_start_date.strftime(
'%Y-%m-%d %H:%M:%S')
end_date = jira_data['sprint']['endDate']
input_end_date = datetime. \
strptime(end_date, '%Y-%m-%dT%H:%M:%S.%fZ')
jira_end_date = input_end_date.strftime(
'%Y-%m-%d %H:%M:%S')
sprint_in_odoo.write({
'start_date': jira_start_date,
'end_date': jira_end_date,
'sprint_goal': jira_data['sprint']['goal'],
'state': 'ongoing'
})
def sprint_closed(self, jira_data):
"""function to close sprint which is created using webhook response"""
sprint_in_odoo = self.env['jira.sprint'].sudo().search(
[('sprint_id_jira', '=', jira_data['sprint']['id'])])
if sprint_in_odoo:
sprint_in_odoo.write({'state': 'completed'})
self.env['project.task'].sudo().search(
[('stage_id.name', '=', 'Done'),
('sprint_id', '=', sprint_in_odoo.id)]).unlink()