# -*- coding: utf-8 -*- ################################################################################ # # Cybrosys Technologies Pvt. Ltd. # # Copyright (C) 2024-TODAY Cybrosys Technologies() # 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 . # ################################################################################ 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()