Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[12.0][IMP] connector_jira: worklog/timesheet date timezone #26

Merged
merged 1 commit into from
Nov 20, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FTR this is not needed as we use the bot to merge ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:) Old habit :)

'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,
}]
)

alexey-pelykh marked this conversation as resolved.
Show resolved Hide resolved
@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