Add HarvestUser.person
association
importer does not set this; you must do so manually
This commit is contained in:
parent
3883a8551f
commit
ec78f8c9c4
|
@ -0,0 +1,35 @@
|
||||||
|
# -*- coding: utf-8; -*-
|
||||||
|
"""add harvest_user.person
|
||||||
|
|
||||||
|
Revision ID: 6bc1cb21d920
|
||||||
|
Revises: 5505c0e60d28
|
||||||
|
Create Date: 2022-01-30 16:49:32.271745
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '6bc1cb21d920'
|
||||||
|
down_revision = '5505c0e60d28'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
import rattail.db.types
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
|
||||||
|
# harvest_user
|
||||||
|
op.add_column('harvest_user', sa.Column('person_uuid', sa.String(length=32), nullable=True))
|
||||||
|
op.create_foreign_key('harvest_user_fk_person', 'harvest_user', 'person', ['person_uuid'], ['uuid'])
|
||||||
|
op.add_column('harvest_user_version', sa.Column('person_uuid', sa.String(length=32), autoincrement=False, nullable=True))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
|
||||||
|
# harvest_user
|
||||||
|
op.drop_column('harvest_user_version', 'person_uuid')
|
||||||
|
op.drop_constraint('harvest_user_fk_person', 'harvest_user', type_='foreignkey')
|
||||||
|
op.drop_column('harvest_user', 'person_uuid')
|
|
@ -39,6 +39,7 @@ class HarvestUser(model.Base):
|
||||||
"""
|
"""
|
||||||
__tablename__ = 'harvest_user'
|
__tablename__ = 'harvest_user'
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
|
sa.ForeignKeyConstraint(['person_uuid'], ['person.uuid'], name='harvest_user_fk_person'),
|
||||||
sa.UniqueConstraint('id', name='harvest_user_uq_id'),
|
sa.UniqueConstraint('id', name='harvest_user_uq_id'),
|
||||||
)
|
)
|
||||||
__versioned__ = {}
|
__versioned__ = {}
|
||||||
|
@ -90,6 +91,19 @@ class HarvestUser(model.Base):
|
||||||
|
|
||||||
updated_at = sa.Column(sa.DateTime(), nullable=True)
|
updated_at = sa.Column(sa.DateTime(), nullable=True)
|
||||||
|
|
||||||
|
person_uuid = sa.Column(sa.String(length=32), nullable=True)
|
||||||
|
person = orm.relationship(
|
||||||
|
model.Person,
|
||||||
|
doc="""
|
||||||
|
Reference to the person associated with this Harvest user.
|
||||||
|
""",
|
||||||
|
backref=orm.backref(
|
||||||
|
'harvest_users',
|
||||||
|
doc="""
|
||||||
|
List of all Harvest user accounts for the person.
|
||||||
|
""")
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return normalize_full_name(self.first_name, self.last_name)
|
return normalize_full_name(self.first_name, self.last_name)
|
||||||
|
|
||||||
|
|
27
rattail_harvest/harvest/importing/__init__.py
Normal file
27
rattail_harvest/harvest/importing/__init__.py
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
# -*- coding: utf-8; -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2022 Lance Edgar
|
||||||
|
#
|
||||||
|
# This file is part of Rattail.
|
||||||
|
#
|
||||||
|
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||||
|
# terms of the GNU General Public License as published by the Free Software
|
||||||
|
# Foundation, either version 3 of the License, or (at your option) any later
|
||||||
|
# version.
|
||||||
|
#
|
||||||
|
# Rattail 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 General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License along with
|
||||||
|
# Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
"""
|
||||||
|
Exporting data to Harvest
|
||||||
|
"""
|
||||||
|
|
||||||
|
from . import model
|
120
rattail_harvest/harvest/importing/model.py
Normal file
120
rattail_harvest/harvest/importing/model.py
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
# -*- coding: utf-8; -*-
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# Rattail -- Retail Software Framework
|
||||||
|
# Copyright © 2010-2022 Lance Edgar
|
||||||
|
#
|
||||||
|
# This file is part of Rattail.
|
||||||
|
#
|
||||||
|
# Rattail is free software: you can redistribute it and/or modify it under the
|
||||||
|
# terms of the GNU General Public License as published by the Free Software
|
||||||
|
# Foundation, either version 3 of the License, or (at your option) any later
|
||||||
|
# version.
|
||||||
|
#
|
||||||
|
# Rattail 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 General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License along with
|
||||||
|
# Rattail. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
"""
|
||||||
|
Harvest model importers
|
||||||
|
"""
|
||||||
|
|
||||||
|
from rattail import importing
|
||||||
|
from rattail_harvest.harvest.webapi import make_harvest_webapi
|
||||||
|
|
||||||
|
|
||||||
|
class ToHarvest(importing.Importer):
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
super(ToHarvest, self).setup()
|
||||||
|
self.webapi = make_harvest_webapi(self.config)
|
||||||
|
|
||||||
|
|
||||||
|
class TimeEntryImporter(ToHarvest):
|
||||||
|
"""
|
||||||
|
Harvest time entry data importer.
|
||||||
|
"""
|
||||||
|
model_name = 'TimeEntry'
|
||||||
|
supported_fields = [
|
||||||
|
'user_id',
|
||||||
|
'project_id',
|
||||||
|
'task_id',
|
||||||
|
'spent_date',
|
||||||
|
# 'started_time',
|
||||||
|
# 'ended_time',
|
||||||
|
'hours',
|
||||||
|
'notes',
|
||||||
|
]
|
||||||
|
caches_local_data = True
|
||||||
|
|
||||||
|
def cache_local_data(self, host_data=None):
|
||||||
|
"""
|
||||||
|
Fetch existing time entries from Harvest.
|
||||||
|
"""
|
||||||
|
cache = {}
|
||||||
|
entries = self.webapi.get_time_entries(**{'from': self.start_date,
|
||||||
|
'to': self.end_date})
|
||||||
|
for entry in entries:
|
||||||
|
data = self.normalize_local_object(entry)
|
||||||
|
if data:
|
||||||
|
normal = self.normalize_cache_object(entry, data)
|
||||||
|
key = self.get_cache_key(entry, normal)
|
||||||
|
cache[key] = normal
|
||||||
|
return cache
|
||||||
|
|
||||||
|
def normalize_local_object(self, entry):
|
||||||
|
data = {
|
||||||
|
'project_id': entry['project']['id'],
|
||||||
|
'task_id': entry['task']['id'],
|
||||||
|
'spent_date': entry['spent_date'],
|
||||||
|
# 'started_time': entry['started_time'],
|
||||||
|
# 'ended_time': entry['ended_time'],
|
||||||
|
'hours': entry['hours'],
|
||||||
|
'notes': entry['notes'],
|
||||||
|
}
|
||||||
|
|
||||||
|
if 'user_id' in self.fields:
|
||||||
|
data['user_id'] = entry['user']['id']
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def create_object(self, key, host_data):
|
||||||
|
if self.dry_run:
|
||||||
|
# mock out return value
|
||||||
|
result = dict(host_data)
|
||||||
|
if 'user_id' in self.fields:
|
||||||
|
result['user'] = {'id': result['user_id']}
|
||||||
|
result['project'] = {'id': result['project_id']}
|
||||||
|
result['task'] = {'id': result['task_id']}
|
||||||
|
return result
|
||||||
|
|
||||||
|
kwargs = {
|
||||||
|
'project_id': host_data['project_id'],
|
||||||
|
'task_id': host_data['task_id'],
|
||||||
|
'spent_date': host_data['spent_date'],
|
||||||
|
# 'started_time': host_data['started_time'],
|
||||||
|
# 'ended_time': host_data['ended_time'],
|
||||||
|
'hours': host_data['hours'],
|
||||||
|
'notes': host_data['notes'],
|
||||||
|
}
|
||||||
|
if 'user_id' in self.fields:
|
||||||
|
kwargs['user_id'] = host_data['user_id']
|
||||||
|
entry = self.webapi.put_time_entry(**kwargs)
|
||||||
|
return entry
|
||||||
|
|
||||||
|
def update_object(self, obj, host_data, local_data=None, all_fields=False):
|
||||||
|
if self.dry_run:
|
||||||
|
return obj
|
||||||
|
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def delete_object(self, obj):
|
||||||
|
if self.dry_run:
|
||||||
|
return True
|
||||||
|
|
||||||
|
raise NotImplementedError
|
|
@ -167,3 +167,12 @@ class HarvestWebAPI(object):
|
||||||
raise ValueError("must provide all of: {}".format(', '.join(required)))
|
raise ValueError("must provide all of: {}".format(', '.join(required)))
|
||||||
response = self.post('/time_entries', params=kwargs)
|
response = self.post('/time_entries', params=kwargs)
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
|
|
||||||
|
def make_harvest_webapi(config):
|
||||||
|
access_token = config.require('harvest', 'api.access_token')
|
||||||
|
account_id = config.require('harvest', 'api.account_id')
|
||||||
|
user_agent = config.require('harvest', 'api.user_agent')
|
||||||
|
return HarvestWebAPI(access_token=access_token,
|
||||||
|
account_id=account_id,
|
||||||
|
user_agent=user_agent)
|
||||||
|
|
|
@ -33,7 +33,7 @@ import sqlalchemy as sa
|
||||||
from rattail import importing
|
from rattail import importing
|
||||||
from rattail.util import OrderedDict
|
from rattail.util import OrderedDict
|
||||||
from rattail_harvest import importing as rattail_harvest_importing
|
from rattail_harvest import importing as rattail_harvest_importing
|
||||||
from rattail_harvest.harvest.webapi import HarvestWebAPI
|
from rattail_harvest.harvest.webapi import make_harvest_webapi
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
@ -71,13 +71,7 @@ class FromHarvest(importing.Importer):
|
||||||
|
|
||||||
def setup(self):
|
def setup(self):
|
||||||
super(FromHarvest, self).setup()
|
super(FromHarvest, self).setup()
|
||||||
|
self.webapi = make_harvest_webapi(self.config)
|
||||||
access_token = self.config.require('harvest', 'api.access_token')
|
|
||||||
account_id = self.config.require('harvest', 'api.account_id')
|
|
||||||
user_agent = self.config.require('harvest', 'api.user_agent')
|
|
||||||
self.webapi = HarvestWebAPI(access_token=access_token,
|
|
||||||
account_id=account_id,
|
|
||||||
user_agent=user_agent)
|
|
||||||
|
|
||||||
def time_from_harvest(self, value):
|
def time_from_harvest(self, value):
|
||||||
# all harvest times appear to come as UTC, so no conversion needed
|
# all harvest times appear to come as UTC, so no conversion needed
|
||||||
|
@ -105,6 +99,9 @@ class HarvestUserImporter(FromHarvest, rattail_harvest_importing.model.HarvestUs
|
||||||
def supported_fields(self):
|
def supported_fields(self):
|
||||||
fields = list(super(HarvestUserImporter, self).supported_fields)
|
fields = list(super(HarvestUserImporter, self).supported_fields)
|
||||||
|
|
||||||
|
# this is for local tracking only; is not in harvest
|
||||||
|
fields.remove('person_uuid')
|
||||||
|
|
||||||
# this used to be in harvest i thought, but is no longer?
|
# this used to be in harvest i thought, but is no longer?
|
||||||
fields.remove('name')
|
fields.remove('name')
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue