Skip to content

Commit

Permalink
Merge PR #26 into 12.0
Browse files Browse the repository at this point in the history
Signed-off-by guewen
  • Loading branch information
OCA-git-bot committed Nov 20, 2019
2 parents dbc5265 + d29b6b1 commit 3369fa4
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 17 deletions.
2 changes: 1 addition & 1 deletion connector_jira/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

{
'name': 'JIRA Connector',
'version': '12.0.1.2.0',
'version': '12.0.1.3.0',
'author': 'Camptocamp,Odoo Community Association (OCA)',
'license': 'AGPL-3',
'category': 'Connector',
Expand Down
22 changes: 11 additions & 11 deletions connector_jira/components/mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,33 +74,33 @@ def modifier(self, record, to_attr):
return modifier


def iso8601_to_local_date(isodate):
""" Returns the local date from an iso8601 date
def iso8601_to_naive_date(isodate):
""" Returns the naive date from an iso8601 date
Keep only the date, when we want to keep only the local date.
Keep only the date, when we want to keep only the naive date.
It's safe to extract it directly from the tz-aware timestamp.
Example with 2014-10-07T00:34:59+0200: we want 2014-10-07 and not
2014-10-06 that we would have using the timestamp converted to UTC.
"""
local_date = isodate[:10]
return datetime.strptime(local_date, '%Y-%m-%d').date()
naive_date = isodate[:10]
return datetime.strptime(naive_date, '%Y-%m-%d').date()


def iso8601_local_date(field):
def iso8601_naive_date(field):
""" A modifier intended to be used on the ``direct`` mappings for
importers.
A JIRA datetime is formatted using the ISO 8601 format.
Returns the local date from an iso8601 datetime.
Returns the naive date from an iso8601 datetime.
Keep only the date, when we want to keep only the local date.
Keep only the date, when we want to keep only the naive date.
It's safe to extract it directly from the tz-aware timestamp.
Example with 2014-10-07T00:34:59+0200: we want 2014-10-07 and not
2014-10-06 that we would have using the timestamp converted to UTC.
Usage::
direct = [(iso8601_local_date('name'), 'name')]
direct = [(iso8601_naive_date('name'), 'name')]
:param field: name of the source field in the record
Expand All @@ -110,8 +110,8 @@ def modifier(self, record, to_attr):
value = record.get(field)
if not value:
return False
utc_date = iso8601_to_local_date(value)
return fields.Date.to_string(utc_date)
naive_date = iso8601_to_naive_date(value)
return fields.Date.to_string(naive_date)
return modifier


Expand Down
17 changes: 15 additions & 2 deletions connector_jira/models/account_analytic_line/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).

import logging
from pytz import timezone, utc

from odoo import _
from odoo.addons.connector.exception import MappingError
from odoo.addons.connector.components.mapper import mapping, only_create
from odoo.addons.component.core import Component
from ...components.mapper import (
iso8601_local_date, iso8601_to_utc_datetime, whenempty
iso8601_to_naive_date, iso8601_to_utc_datetime, whenempty
)
from ...fields import MilliDatetime

Expand All @@ -23,7 +24,6 @@ class AnalyticLineMapper(Component):

direct = [
(whenempty('comment', _('missing description')), 'name'),
(iso8601_local_date('started'), 'date'),
]

@only_create
Expand All @@ -49,6 +49,19 @@ def issue(self, record):
refs['jira_epic_issue_key'] = issue['fields'][epic_field_name]
return refs

@mapping
def date(self, record):
mode = self.backend_record.worklog_date_timezone_mode
started = record['started']
if not mode or mode == 'naive':
return {'date': iso8601_to_naive_date(started)}
started = iso8601_to_utc_datetime(started).replace(tzinfo=utc)
if mode == 'user':
tz = timezone(record['author']['timeZone'])
elif mode == 'specific':
tz = timezone(self.backend_record.worklog_date_timezone)
return {'date': started.astimezone(tz).date()}

@mapping
def duration(self, record):
spent = float(record['timeSpentSeconds'])
Expand Down
28 changes: 28 additions & 0 deletions connector_jira/models/jira_backend/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import binascii
import logging
import json
import pytz
import urllib.parse

from contextlib import contextmanager, closing
Expand Down Expand Up @@ -89,6 +90,26 @@ def _default_consumer_key(self):
"project by: 1. linking the expected project with the Jira one, "
"2. using 'Refresh Worklogs from Jira' on the timesheet lines."
)
worklog_date_timezone_mode = fields.Selection(
selection=[
('naive', 'As-is (naive)'),
('user', 'Jira User'),
('specific', 'Specific'),
],
default='naive',
help=(
'Worklog/Timesheet date timezone modes:\n'
' - As-is (naive): ignore timezone information\n'
' - Jira User: use author\'s timezone\n'
' - Specific: use pre-configured timezone\n'
),
)
worklog_date_timezone = fields.Selection(
selection=lambda self: [(x, x) for x in pytz.all_timezones],
default=(
lambda self: self._context.get('tz') or self.env.user.tz or 'UTC'
),
)
state = fields.Selection(
selection=[('authenticate', 'Authenticate'),
('setup', 'Setup'),
Expand Down Expand Up @@ -428,6 +449,13 @@ def onchange_odoo_webhook_base_url(self):
'the Webhooks again.')
return {'warning': {'title': _('Warning'), 'message': msg}}

@api.onchange('worklog_date_timezone_mode')
def _onchange_worklog_date_import_timezone_mode(self):
for jira_backend in self:
if jira_backend.worklog_date_timezone_mode == 'specific':
continue
jira_backend.worklog_date_timezone = False

@api.multi
def delete_webhooks(self):
self.ensure_one()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ interactions:
body: {string: '{"expand":"renderedFields,names,schema,operations,editmeta,changelog,versionedRepresentations","id":"10000","self":"http://jira:8080/rest/api/2/issue/10000","key":"TEST-1","fields":{"issuetype":{"self":"http://jira:8080/rest/api/2/issuetype/10000","id":"10000","description":"Created
by Jira Software - do not edit or delete. Issue type for a big user story
that needs to be broken down.","iconUrl":"http://jira:8080/images/icons/issuetypes/epic.svg","name":"Epic","subtask":false},"timespent":3600,"project":{"self":"http://jira:8080/rest/api/2/project/10000","id":"10000","key":"TEST","name":"TEST","avatarUrls":{"48x48":"http://jira:8080/secure/projectavatar?avatarId=10324","24x24":"http://jira:8080/secure/projectavatar?size=small&avatarId=10324","16x16":"http://jira:8080/secure/projectavatar?size=xsmall&avatarId=10324","32x32":"http://jira:8080/secure/projectavatar?size=medium&avatarId=10324"}},"fixVersions":[],"aggregatetimespent":3600,"resolution":null,"customfield_10104":"ghx-label-1","customfield_10105":"0|hzzzzz:","customfield_10106":null,"resolutiondate":null,"workratio":-1,"lastViewed":"2019-04-08T13:51:13.798+0000","watches":{"self":"http://jira:8080/rest/api/2/issue/TEST-1/watchers","watchCount":1,"isWatching":true},"created":"2019-04-04T09:31:27.779+0000","priority":{"self":"http://jira:8080/rest/api/2/priority/3","iconUrl":"http://jira:8080/images/icons/priorities/medium.svg","name":"Medium","id":"3"},"customfield_10100":null,"customfield_10101":null,"customfield_10102":{"self":"http://jira:8080/rest/api/2/customFieldOption/10000","value":"To
Do","id":"10000"},"labels":[],"customfield_10103":"Epic1","timeestimate":0,"aggregatetimeoriginalestimate":null,"versions":[],"issuelinks":[],"assignee":null,"updated":"2019-04-04T11:01:47.600+0000","status":{"self":"http://jira:8080/rest/api/2/status/10000","description":"","iconUrl":"http://jira:8080/","name":"To
Do","id":"10000"},"labels":[],"customfield_10103":"Epic1","timeestimate":0,"aggregatetimeoriginalestimate":null,"versions":[],"issuelinks":[],"assignee":null,"updated":"2019-04-04T09:01:47.600+0000","status":{"self":"http://jira:8080/rest/api/2/status/10000","description":"","iconUrl":"http://jira:8080/","name":"To
Do","id":"10000","statusCategory":{"self":"http://jira:8080/rest/api/2/statuscategory/2","id":2,"key":"new","colorName":"blue-gray","name":"To
Do"}},"components":[],"timeoriginalestimate":null,"description":null,"timetracking":{"remainingEstimate":"0m","timeSpent":"1h","remainingEstimateSeconds":0,"timeSpentSeconds":3600},"customfield_10203":null,"customfield_10204":null,"customfield_10205":null,"customfield_10206":null,"attachment":[],"customfield_10207":null,"aggregatetimeestimate":0,"customfield_10208":null,"summary":"Epic1","creator":{"self":"http://jira:8080/rest/api/2/user?username=gbaconnier","name":"gbaconnier","key":"gbaconnier","emailAddress":"guewen.baconnier@camptocamp.com","avatarUrls":{"48x48":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=48","24x24":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=24","16x16":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=16","32x32":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=32"},"displayName":"Guewen
Baconnier","active":true,"timeZone":"GMT"},"subtasks":[],"reporter":{"self":"http://jira:8080/rest/api/2/user?username=gbaconnier","name":"gbaconnier","key":"gbaconnier","emailAddress":"guewen.baconnier@camptocamp.com","avatarUrls":{"48x48":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=48","24x24":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=24","16x16":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=16","32x32":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=32"},"displayName":"Guewen
Expand All @@ -324,7 +324,7 @@ interactions:
branch=com.atlassian.jira.plugin.devstatus.rest.SummaryItemBean@7af21e5f[overall=com.atlassian.jira.plugin.devstatus.summary.beans.BranchOverallBean@7a591452[count=0,lastUpdated=<null>,lastUpdatedTimestamp=<null>],byInstanceType={}]},errors=[],configErrors=[]],
devSummaryJson={\"cachedValue\":{\"errors\":[],\"configErrors\":[],\"summary\":{\"pullrequest\":{\"overall\":{\"count\":0,\"lastUpdated\":null,\"stateCount\":0,\"state\":\"OPEN\",\"details\":{\"openCount\":0,\"mergedCount\":0,\"declinedCount\":0,\"total\":0},\"open\":true},\"byInstanceType\":{}},\"build\":{\"overall\":{\"count\":0,\"lastUpdated\":null,\"failedBuildCount\":0,\"successfulBuildCount\":0,\"unknownBuildCount\":0},\"byInstanceType\":{}},\"review\":{\"overall\":{\"count\":0,\"lastUpdated\":null,\"stateCount\":0,\"state\":null,\"dueDate\":null,\"overDue\":false,\"completed\":false},\"byInstanceType\":{}},\"deployment-environment\":{\"overall\":{\"count\":0,\"lastUpdated\":null,\"topEnvironments\":[],\"showProjects\":false,\"successfulCount\":0},\"byInstanceType\":{}},\"repository\":{\"overall\":{\"count\":0,\"lastUpdated\":null},\"byInstanceType\":{}},\"branch\":{\"overall\":{\"count\":0,\"lastUpdated\":null},\"byInstanceType\":{}}}},\"isStale\":false}}","aggregateprogress":{"progress":3600,"total":3600,"percent":100},"customfield_10200":null,"customfield_10201":[],"customfield_10202":null,"environment":null,"duedate":null,"progress":{"progress":3600,"total":3600,"percent":100},"comment":{"comments":[],"maxResults":0,"total":0,"startAt":0},"votes":{"self":"http://jira:8080/rest/api/2/issue/TEST-1/votes","votes":0,"hasVoted":false},"worklog":{"startAt":0,"maxResults":20,"total":1,"worklogs":[{"self":"http://jira:8080/rest/api/2/issue/10000/worklog/10000","author":{"self":"http://jira:8080/rest/api/2/user?username=gbaconnier","name":"gbaconnier","key":"gbaconnier","emailAddress":"guewen.baconnier@camptocamp.com","avatarUrls":{"48x48":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=48","24x24":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=24","16x16":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=16","32x32":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=32"},"displayName":"Guewen
Baconnier","active":true,"timeZone":"GMT"},"updateAuthor":{"self":"http://jira:8080/rest/api/2/user?username=gbaconnier","name":"gbaconnier","key":"gbaconnier","emailAddress":"guewen.baconnier@camptocamp.com","avatarUrls":{"48x48":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=48","24x24":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=24","16x16":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=16","32x32":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=32"},"displayName":"Guewen
Baconnier","active":true,"timeZone":"GMT"},"comment":"write tests","created":"2019-04-04T11:01:47.597+0000","updated":"2019-04-04T11:01:47.597+0000","started":"2019-04-04T11:01:00.000+0000","timeSpent":"1h","timeSpentSeconds":3600,"id":"10000","issueId":"10000"}]}},"renderedFields":{"issuetype":null,"timespent":"1
Baconnier","active":true,"timeZone":"GMT"},"comment":"write tests","created":"2019-04-04T04:01:47.597+0800","updated":"2019-04-04T04:01:47.597+0800","started":"2019-04-04T11:01:00.000+0000","timeSpent":"1h","timeSpentSeconds":3600,"id":"10000","issueId":"10000"}]}},"renderedFields":{"issuetype":null,"timespent":"1
hour","project":null,"fixVersions":null,"aggregatetimespent":"1 hour","resolution":null,"customfield_10104":"ghx-label-1","customfield_10105":null,"customfield_10106":null,"resolutiondate":null,"workratio":null,"lastViewed":"4
days ago 1:51 PM","watches":null,"created":"04/Apr/19 9:31 AM","priority":null,"customfield_10100":null,"customfield_10101":null,"customfield_10102":null,"labels":null,"customfield_10103":"Epic1","timeestimate":"0
minutes","aggregatetimeoriginalestimate":null,"versions":null,"issuelinks":null,"assignee":null,"updated":"04/Apr/19
Expand Down Expand Up @@ -664,7 +664,7 @@ interactions:
response:
body: {string: '{"self":"http://jira:8080/rest/api/2/issue/10000/worklog/10000","author":{"self":"http://jira:8080/rest/api/2/user?username=gbaconnier","name":"gbaconnier","key":"gbaconnier","emailAddress":"guewen.baconnier@camptocamp.com","avatarUrls":{"48x48":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=48","24x24":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=24","16x16":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=16","32x32":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=32"},"displayName":"Guewen
Baconnier","active":true,"timeZone":"GMT"},"updateAuthor":{"self":"http://jira:8080/rest/api/2/user?username=gbaconnier","name":"gbaconnier","key":"gbaconnier","emailAddress":"guewen.baconnier@camptocamp.com","avatarUrls":{"48x48":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=48","24x24":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=24","16x16":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=16","32x32":"https://www.gravatar.com/avatar/ad252192c3f73885676b7d2e850ad13c?d=mm&s=32"},"displayName":"Guewen
Baconnier","active":true,"timeZone":"GMT"},"comment":"write tests","created":"2019-04-04T11:01:47.597+0000","updated":"2019-04-04T11:01:47.597+0000","started":"2019-04-04T11:01:00.000+0000","timeSpent":"1h","timeSpentSeconds":3600,"id":"10000","issueId":"10000"}'}
Baconnier","active":true,"timeZone":"GMT"},"comment":"write tests","created":"2019-04-04T04:01:47.597+0800","updated":"2019-04-04T04:01:47.597+0800","started":"2019-04-04T04:01:47.597+0800","timeSpent":"1h","timeSpentSeconds":3600,"id":"10000","issueId":"10000"}'}
headers:
Cache-Control: ['no-cache, no-store, no-transform']
Content-Security-Policy: [frame-ancestors 'self']
Expand Down
82 changes: 82 additions & 0 deletions connector_jira/tests/test_import_analytic_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,85 @@ def _test_import_worklog(self, expected_project, expected_task):
'user_id': self.env.user.id,
}]
)

@recorder.use_cassette('test_import_worklog.yaml')
def test_import_worklog_naive(self):
jira_worklog_id = jira_issue_id = '10000'
self.backend_record.worklog_date_timezone_mode = 'naive'
binding = self._setup_import_worklog(
self.task, jira_issue_id, jira_worklog_id)
self.assertRecordValues(
binding,
[{
'account_id': self.project.analytic_account_id.id,
'backend_id': self.backend_record.id,
'date': date(2019, 4, 4),
'employee_id': self.env.user.employee_ids[0].id,
'external_id': jira_worklog_id,
'jira_epic_issue_key': False,
'jira_issue_id': jira_issue_id,
'jira_issue_key': 'TEST-1',
'jira_issue_type_id': self.epic_issue_type.id,
'name': 'write tests',
'project_id': self.project.id,
'tag_ids': [],
'task_id': self.task.id if self.task else False,
'unit_amount': 1.0,
'user_id': self.env.user.id,
}]
)

@recorder.use_cassette('test_import_worklog.yaml')
def test_import_worklog_user(self):
jira_worklog_id = jira_issue_id = '10000'
self.backend_record.worklog_date_timezone_mode = 'user'
binding = self._setup_import_worklog(
self.task, jira_issue_id, jira_worklog_id)
self.assertRecordValues(
binding,
[{
'account_id': self.project.analytic_account_id.id,
'backend_id': self.backend_record.id,
'date': date(2019, 4, 3),
'employee_id': self.env.user.employee_ids[0].id,
'external_id': jira_worklog_id,
'jira_epic_issue_key': False,
'jira_issue_id': jira_issue_id,
'jira_issue_key': 'TEST-1',
'jira_issue_type_id': self.epic_issue_type.id,
'name': 'write tests',
'project_id': self.project.id,
'tag_ids': [],
'task_id': self.task.id if self.task else False,
'unit_amount': 1.0,
'user_id': self.env.user.id,
}]
)

@recorder.use_cassette('test_import_worklog.yaml')
def test_import_worklog_specific(self):
jira_worklog_id = jira_issue_id = '10000'
self.backend_record.worklog_date_timezone_mode = 'specific'
self.backend_record.worklog_date_timezone = 'Europe/London'
binding = self._setup_import_worklog(
self.task, jira_issue_id, jira_worklog_id)
self.assertRecordValues(
binding,
[{
'account_id': self.project.analytic_account_id.id,
'backend_id': self.backend_record.id,
'date': date(2019, 4, 3),
'employee_id': self.env.user.employee_ids[0].id,
'external_id': jira_worklog_id,
'jira_epic_issue_key': False,
'jira_issue_id': jira_issue_id,
'jira_issue_key': 'TEST-1',
'jira_issue_type_id': self.epic_issue_type.id,
'name': 'write tests',
'project_id': self.project.id,
'tag_ids': [],
'task_id': self.task.id if self.task else False,
'unit_amount': 1.0,
'user_id': self.env.user.id,
}]
)
17 changes: 17 additions & 0 deletions connector_jira/views/jira_backend_views.xml
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,23 @@
the field will be empty.
</p>
</group>
<group name="worklog" col="4">
<group name="worklog_fields">
<field
name="worklog_date_timezone_mode"
string="Worklog/Timesheet date timezone"
/>
<field
name="worklog_date_timezone"
attrs="{'invisible': [('worklog_date_timezone_mode', '!=', 'specific')], 'required': [('worklog_date_timezone_mode', '=', 'specific')]}"
nolabel="1"
/>
</group>
<div/>
<p class="oe_grey oe_inline">
Configure worklog fields
</p>
</group>
</page>
<page name="issue_type" string="Issue Types" states="running">
<field name="issue_type_ids">
Expand Down

0 comments on commit 3369fa4

Please sign in to comment.