diff --git a/rattail_harvest/db/alembic/versions/6bc1cb21d920_add_harvest_user_person.py b/rattail_harvest/db/alembic/versions/6bc1cb21d920_add_harvest_user_person.py new file mode 100644 index 0000000..31cca85 --- /dev/null +++ b/rattail_harvest/db/alembic/versions/6bc1cb21d920_add_harvest_user_person.py @@ -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') diff --git a/rattail_harvest/db/model/harvest.py b/rattail_harvest/db/model/harvest.py index 4d0e80e..7ac7506 100644 --- a/rattail_harvest/db/model/harvest.py +++ b/rattail_harvest/db/model/harvest.py @@ -39,6 +39,7 @@ class HarvestUser(model.Base): """ __tablename__ = 'harvest_user' __table_args__ = ( + sa.ForeignKeyConstraint(['person_uuid'], ['person.uuid'], name='harvest_user_fk_person'), sa.UniqueConstraint('id', name='harvest_user_uq_id'), ) __versioned__ = {} @@ -90,6 +91,19 @@ class HarvestUser(model.Base): 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): return normalize_full_name(self.first_name, self.last_name) diff --git a/rattail_harvest/harvest/importing/__init__.py b/rattail_harvest/harvest/importing/__init__.py new file mode 100644 index 0000000..e0b3e09 --- /dev/null +++ b/rattail_harvest/harvest/importing/__init__.py @@ -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 . +# +################################################################################ +""" +Exporting data to Harvest +""" + +from . import model diff --git a/rattail_harvest/harvest/importing/model.py b/rattail_harvest/harvest/importing/model.py new file mode 100644 index 0000000..47e85de --- /dev/null +++ b/rattail_harvest/harvest/importing/model.py @@ -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 . +# +################################################################################ +""" +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 diff --git a/rattail_harvest/harvest/webapi.py b/rattail_harvest/harvest/webapi.py index 6107b19..a037f1b 100644 --- a/rattail_harvest/harvest/webapi.py +++ b/rattail_harvest/harvest/webapi.py @@ -167,3 +167,12 @@ class HarvestWebAPI(object): raise ValueError("must provide all of: {}".format(', '.join(required))) response = self.post('/time_entries', params=kwargs) 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) diff --git a/rattail_harvest/importing/harvest.py b/rattail_harvest/importing/harvest.py index cde1e1c..695d04d 100644 --- a/rattail_harvest/importing/harvest.py +++ b/rattail_harvest/importing/harvest.py @@ -33,7 +33,7 @@ import sqlalchemy as sa from rattail import importing from rattail.util import OrderedDict 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__) @@ -71,13 +71,7 @@ class FromHarvest(importing.Importer): def setup(self): super(FromHarvest, self).setup() - - 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) + self.webapi = make_harvest_webapi(self.config) def time_from_harvest(self, value): # 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): 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? fields.remove('name')