rattail-harvest/rattail_harvest/importing/harvest.py
Lance Edgar 3883a8551f Add HarvestProject.deleted flag to track deletions in Harvest
set this flag instead of deleting project, so we do not lose other
info about it.  can delete manually if truly unwanted
2022-01-30 12:14:42 -06:00

249 lines
7.8 KiB
Python

# -*- 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 -> Rattail "cache" data import
"""
import datetime
import decimal
import logging
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
log = logging.getLogger(__name__)
class FromHarvestToRattail(importing.ToRattailHandler):
"""
Import handler for data coming from the Harvest API
"""
host_key = 'harvest'
host_title = "Harvest (API)"
generic_host_title = "Harvest (API)"
def get_importers(self):
importers = OrderedDict()
importers['HarvestUser'] = HarvestUserImporter
importers['HarvestClient'] = HarvestClientImporter
importers['HarvestProject'] = HarvestProjectImporter
importers['HarvestTask'] = HarvestTaskImporter
importers['HarvestTimeEntry'] = HarvestTimeEntryImporter
return importers
class FromHarvest(importing.Importer):
"""
Base class for all Harvest importers
"""
key = 'id'
@property
def supported_fields(self):
fields = list(super(FromHarvest, self).supported_fields)
fields.remove('uuid')
return fields
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)
def time_from_harvest(self, value):
# all harvest times appear to come as UTC, so no conversion needed
value = datetime.datetime.strptime(value, '%Y-%m-%dT%H:%M:%SZ')
return value
def normalize_host_object(self, obj):
data = dict(obj)
if 'created_at' in self.fields:
data['created_at'] = self.time_from_harvest(data['created_at'])
if 'updated_at' in self.fields:
data['updated_at'] = self.time_from_harvest(data['updated_at'])
return data
class HarvestUserImporter(FromHarvest, rattail_harvest_importing.model.HarvestUserImporter):
"""
Import user data from Harvest
"""
@property
def supported_fields(self):
fields = list(super(HarvestUserImporter, self).supported_fields)
# this used to be in harvest i thought, but is no longer?
fields.remove('name')
return fields
def get_host_objects(self):
return self.webapi.get_users()['users']
class HarvestClientImporter(FromHarvest, rattail_harvest_importing.model.HarvestClientImporter):
"""
Import client data from Harvest
"""
def get_host_objects(self):
return self.webapi.get_clients()['clients']
class HarvestProjectImporter(FromHarvest, rattail_harvest_importing.model.HarvestProjectImporter):
"""
Import project data from Harvest
"""
@property
def supported_fields(self):
fields = list(super(HarvestProjectImporter, self).supported_fields)
# this is for local tracking only; is not in harvest
fields.remove('deleted')
return fields
def cache_query(self):
model = self.model
return self.session.query(model.HarvestProject)\
.filter(sa.or_(
model.HarvestProject.deleted == False,
model.HarvestProject.deleted == None))
def get_host_objects(self):
return self.webapi.get_projects()['projects']
def normalize_host_object(self, project):
data = super(HarvestProjectImporter, self).normalize_host_object(project)
if not data:
return
data['client_id'] = project['client']['id']
# cost_budget
cost_budget = data['cost_budget']
if cost_budget is not None:
cost_budget = decimal.Decimal('{:0.2f}'.format(cost_budget))
data['cost_budget'] = cost_budget
# fee
fee = data['fee']
if fee is not None:
fee = decimal.Decimal('{:0.2f}'.format(fee))
data['fee'] = fee
# starts_on
starts_on = data['starts_on']
if starts_on:
starts_on = datetime.datetime.strptime(starts_on, '%Y-%m-%d')
data['starts_on'] = starts_on.date()
# ends_on
ends_on = data['ends_on']
if ends_on:
ends_on = datetime.datetime.strptime(ends_on, '%Y-%m-%d')
data['ends_on'] = ends_on.date()
# over_budget_notification_date
date = data['over_budget_notification_date']
if date:
date = datetime.datetime.strptime(date, '%Y-%m-%d')
data['over_budget_notification_date'] = date.date()
return data
def can_delete_object(self, project, data):
return not project.deleted
def delete_object(self, project):
project.deleted = True
return True
class HarvestTaskImporter(FromHarvest, rattail_harvest_importing.model.HarvestTaskImporter):
"""
Import task data from Harvest
"""
def get_host_objects(self):
return self.webapi.get_tasks()['tasks']
class HarvestTimeEntryImporter(FromHarvest, rattail_harvest_importing.model.HarvestTimeEntryImporter):
"""
Import time entry data from Harvest
"""
def get_host_objects(self):
return self.webapi.get_time_entries(**{'from': self.start_date,
'to': self.end_date})
def normalize_host_object(self, entry):
data = super(HarvestTimeEntryImporter, self).normalize_host_object(entry)
if not data:
return
data['user_id'] = entry['user']['id']
data['client_id'] = entry['client']['id']
data['task_id'] = entry['task']['id']
data['invoice_id'] = entry['invoice']['id'] if entry['invoice'] else None
# project_id
if 'project_id' in self.fields:
data['project_id'] = entry['project']['id']
project = self.get_harvest_project(data['project_id'])
if not project:
logger = log.warning if self.warn_for_unknown_project else log.debug
logger("time entry references non-existent project id %s: %s",
data['project_id'], entry)
if not self.auto_create_unknown_project:
data['project_id'] = None
# spent_date
spent_date = data['spent_date']
if spent_date:
spent_date = datetime.datetime.strptime(spent_date, '%Y-%m-%d')
data['spent_date'] = spent_date.date()
# hours
hours = data['hours']
if hours is not None:
hours = decimal.Decimal('{:0.2f}'.format(hours))
data['hours'] = hours
return data