Compare commits
42 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e0b8797046 | ||
![]() |
69ce07a778 | ||
![]() |
df324159ba | ||
![]() |
9687c85529 | ||
![]() |
0521f5e38c | ||
![]() |
ff202415e3 | ||
![]() |
d419bc1854 | ||
![]() |
ffe33b20bc | ||
![]() |
70f496893b | ||
![]() |
5ae7db78a5 | ||
![]() |
1cfd30a211 | ||
![]() |
02298a7a29 | ||
![]() |
8784579e27 | ||
![]() |
d7e840138b | ||
![]() |
da408a66bb | ||
![]() |
9bf9a17bf6 | ||
![]() |
bdb8b22ef4 | ||
![]() |
fe0daf00bc | ||
![]() |
aa87ce57be | ||
![]() |
509405cb34 | ||
![]() |
e58d843ee4 | ||
![]() |
2f21e574ae | ||
![]() |
2fa7ef5e71 | ||
![]() |
d508ca225b | ||
![]() |
44574d9ea6 | ||
![]() |
0164336784 | ||
![]() |
e239ea70e4 | ||
![]() |
782cb1fcec | ||
![]() |
03066f1135 | ||
![]() |
d84cc7a9d9 | ||
![]() |
0ee4d52145 | ||
![]() |
c6332be453 | ||
![]() |
d8e9714771 | ||
![]() |
e79686b5b7 | ||
![]() |
ac653d25f1 | ||
![]() |
caa1ef93b7 | ||
![]() |
63d238d307 | ||
![]() |
14f9dfdaa9 | ||
![]() |
1f54ddc9e4 | ||
![]() |
ec78f8c9c4 | ||
![]() |
3883a8551f | ||
![]() |
259d3b0f33 |
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1 +1,4 @@
|
|||
*~
|
||||
*.pyc
|
||||
dist/
|
||||
rattail_harvest.egg-info/
|
||||
|
|
30
CHANGELOG.md
30
CHANGELOG.md
|
@ -5,6 +5,36 @@ All notable changes to rattail-harvest will be documented in this file.
|
|||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## v0.3.2 (2024-08-18)
|
||||
|
||||
### Fix
|
||||
|
||||
- avoid deprecated base class for config extension
|
||||
|
||||
## v0.3.1 (2024-07-01)
|
||||
|
||||
### Fix
|
||||
|
||||
- remove legacy command definitions
|
||||
|
||||
## v0.3.0 (2024-06-10)
|
||||
|
||||
### Feat
|
||||
|
||||
- switch from setup.cfg to pyproject.toml + hatchling
|
||||
|
||||
## [0.2.1] - 2024-06-06
|
||||
### Changed
|
||||
- Add alembic scripts to project manifest.
|
||||
|
||||
## [0.2.0] - 2024-06-06
|
||||
### Changed
|
||||
- Add typer equivalents for `rattail` commands.
|
||||
|
||||
## [0.1.2] - 2023-11-18
|
||||
### Changed
|
||||
- Catch-up release, with various schema changes etc.
|
||||
|
||||
## [0.1.1] - 2022-01-29
|
||||
### Added
|
||||
- Initial version.
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
include *.md
|
||||
include *.rst
|
||||
|
||||
recursive-include rattail_harvest/db/alembic *.mako
|
||||
recursive-include rattail_harvest/db/alembic *.py
|
||||
|
|
11
README.md
Normal file
11
README.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
|
||||
# rattail-harvest
|
||||
|
||||
Rattail is a retail software framework, released under the GNU General
|
||||
Public License.
|
||||
|
||||
This package contains software interfaces for
|
||||
[Harvest](https://www.getharvest.com/).
|
||||
|
||||
Please see the [Rattail Project](https://rattailproject.org/) for more
|
||||
information.
|
14
README.rst
14
README.rst
|
@ -1,14 +0,0 @@
|
|||
|
||||
rattail-harvest
|
||||
===============
|
||||
|
||||
Rattail is a retail software framework, released under the GNU General
|
||||
Public License.
|
||||
|
||||
This package contains software interfaces for `Harvest`_.
|
||||
|
||||
.. _`Harvest`: https://www.getharvest.com/
|
||||
|
||||
Please see the `Rattail Project`_ for more information.
|
||||
|
||||
.. _`Rattail Project`: https://rattailproject.org/
|
53
pyproject.toml
Normal file
53
pyproject.toml
Normal file
|
@ -0,0 +1,53 @@
|
|||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
|
||||
[project]
|
||||
name = "rattail-harvest"
|
||||
version = "0.3.2"
|
||||
description = "Rattail integration package for Harvest"
|
||||
readme = "README.md"
|
||||
authors = [{name = "Lance Edgar", email = "lance@edbob.org"}]
|
||||
license = {text = "GNU GPL v3+"}
|
||||
classifiers = [
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Environment :: Console",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
|
||||
"Natural Language :: English",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Topic :: Office/Business",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
]
|
||||
dependencies = [
|
||||
"invoke",
|
||||
"rattail[db]",
|
||||
]
|
||||
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://rattailproject.org"
|
||||
Repository = "https://forgejo.wuttaproject.org/rattail/rattail-harvest"
|
||||
Changelog = "https://forgejo.wuttaproject.org/rattail/rattail-harvest/src/branch/master/CHANGELOG.md"
|
||||
|
||||
|
||||
[project.entry-points."rattail.typer_imports"]
|
||||
rattail_harvest = "rattail_harvest.commands"
|
||||
|
||||
|
||||
[project.entry-points."rattail.config.extensions"]
|
||||
rattail_harvest = "rattail_harvest.config:RattailHarvestExtension"
|
||||
|
||||
|
||||
[project.entry-points."rattail.importing"]
|
||||
"to_rattail.from_harvest.import" = "rattail_harvest.importing.harvest:FromHarvestToRattail"
|
||||
|
||||
|
||||
[tool.commitizen]
|
||||
version_provider = "pep621"
|
||||
tag_format = "v$version"
|
||||
update_changelog_on_bump = true
|
|
@ -1,3 +1,6 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
|
||||
__version__ = '0.1.1'
|
||||
from importlib.metadata import version
|
||||
|
||||
|
||||
__version__ = version('rattail-harvest')
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2022 Lance Edgar
|
||||
# Copyright © 2010-2024 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
|
@ -24,13 +24,25 @@
|
|||
rattail-harvest commands
|
||||
"""
|
||||
|
||||
from rattail import commands
|
||||
import typer
|
||||
|
||||
from rattail.commands import rattail_typer
|
||||
from rattail.commands.typer import importer_command, typer_get_runas_user
|
||||
from rattail.commands.importing import ImportCommandHandler
|
||||
|
||||
|
||||
class ImportHarvest(commands.ImportSubcommand):
|
||||
@rattail_typer.command()
|
||||
@importer_command
|
||||
def import_harvest(
|
||||
ctx: typer.Context,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
Import data to Rattail, from Harvest API
|
||||
"""
|
||||
name = 'import-harvest'
|
||||
description = __doc__.strip()
|
||||
handler_key = 'to_rattail.from_harvest.import'
|
||||
config = ctx.parent.rattail_config
|
||||
progress = ctx.parent.rattail_progress
|
||||
handler = ImportCommandHandler(
|
||||
config, import_handler_key='to_rattail.from_harvest.import')
|
||||
kwargs['user'] = typer_get_runas_user(ctx)
|
||||
handler.run(kwargs, progress=progress)
|
||||
|
|
42
rattail_harvest/config.py
Normal file
42
rattail_harvest/config.py
Normal file
|
@ -0,0 +1,42 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2024 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/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Config Extension
|
||||
"""
|
||||
|
||||
from wuttjamaican.conf import WuttaConfigExtension
|
||||
|
||||
|
||||
class RattailHarvestExtension(WuttaConfigExtension):
|
||||
"""
|
||||
Config extension for rattail-harvest.
|
||||
"""
|
||||
key = 'rattail_harvest'
|
||||
|
||||
def configure(self, config):
|
||||
|
||||
# rattail import-harvest
|
||||
config.setdefault('rattail.importing', 'to_rattail.from_harvest.import.default_handler',
|
||||
'rattail_harvest.importing.harvest:FromHarvestToRattail')
|
||||
config.setdefault('rattail.importing', 'to_rattail.from_harvest.import.default_cmd',
|
||||
'rattail import-harvest')
|
|
@ -0,0 +1,179 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
"""rename cache tables
|
||||
|
||||
Revision ID: 53c066772ad5
|
||||
Revises: f2a1650e7fbc
|
||||
Create Date: 2023-10-04 15:19:03.857323
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '53c066772ad5'
|
||||
down_revision = 'f2a1650e7fbc'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import rattail.db.types
|
||||
|
||||
|
||||
|
||||
def upgrade():
|
||||
|
||||
##############################
|
||||
# drop all constraints
|
||||
##############################
|
||||
|
||||
# harvest_time_entry
|
||||
op.drop_constraint('harvest_time_entry_fk_user', 'harvest_time_entry', type_='foreignkey')
|
||||
op.drop_constraint('harvest_time_entry_fk_client', 'harvest_time_entry', type_='foreignkey')
|
||||
op.drop_constraint('harvest_time_entry_fk_project', 'harvest_time_entry', type_='foreignkey')
|
||||
op.drop_constraint('harvest_time_entry_fk_task', 'harvest_time_entry', type_='foreignkey')
|
||||
op.drop_constraint('harvest_time_entry_uq_id', 'harvest_time_entry', type_='unique')
|
||||
|
||||
# harvest_task
|
||||
op.drop_constraint('harvest_task_uq_id', 'harvest_task', type_='unique')
|
||||
|
||||
# harvest_project
|
||||
op.drop_constraint('harvest_project_fk_client', 'harvest_project', type_='foreignkey')
|
||||
op.drop_constraint('harvest_project_uq_id', 'harvest_project', type_='unique')
|
||||
|
||||
# harvest_client
|
||||
op.drop_constraint('harvest_client_uq_id', 'harvest_client', type_='unique')
|
||||
|
||||
# harvest_user
|
||||
op.drop_constraint('harvest_user_fk_person', 'harvest_user', type_='foreignkey')
|
||||
op.drop_constraint('harvest_user_uq_id', 'harvest_user', type_='unique')
|
||||
|
||||
##############################
|
||||
# rename all tables
|
||||
##############################
|
||||
|
||||
op.rename_table('harvest_user', 'harvest_cache_user')
|
||||
op.rename_table('harvest_user_version', 'harvest_cache_user_version')
|
||||
op.rename_table('harvest_client', 'harvest_cache_client')
|
||||
op.rename_table('harvest_client_version', 'harvest_cache_client_version')
|
||||
op.rename_table('harvest_project', 'harvest_cache_project')
|
||||
op.rename_table('harvest_project_version', 'harvest_cache_project_version')
|
||||
op.rename_table('harvest_task', 'harvest_cache_task')
|
||||
op.rename_table('harvest_task_version', 'harvest_cache_task_version')
|
||||
op.rename_table('harvest_time_entry', 'harvest_cache_time_entry')
|
||||
op.rename_table('harvest_time_entry_version', 'harvest_cache_time_entry_version')
|
||||
|
||||
##############################
|
||||
# re-create all constraints
|
||||
##############################
|
||||
|
||||
# harvest_cache_user
|
||||
op.create_foreign_key('harvest_cache_user_fk_person',
|
||||
'harvest_cache_user', 'person',
|
||||
['person_uuid'], ['uuid'])
|
||||
op.create_unique_constraint('harvest_cache_user_uq_id', 'harvest_cache_user', ['id'])
|
||||
|
||||
# harvest_cache_client
|
||||
op.create_unique_constraint('harvest_cache_client_uq_id', 'harvest_cache_client', ['id'])
|
||||
|
||||
# harvest_cache_project
|
||||
op.create_foreign_key('harvest_cache_project_fk_client',
|
||||
'harvest_cache_project', 'harvest_cache_client',
|
||||
['client_id'], ['id'])
|
||||
op.create_unique_constraint('harvest_cache_project_uq_id', 'harvest_cache_project', ['id'])
|
||||
|
||||
# harvest_cache_task
|
||||
op.create_unique_constraint('harvest_cache_task_uq_id', 'harvest_cache_task', ['id'])
|
||||
|
||||
# harvest_cache_time_entry
|
||||
op.create_foreign_key('harvest_cache_time_entry_fk_user',
|
||||
'harvest_cache_time_entry', 'harvest_cache_user',
|
||||
['user_id'], ['id'])
|
||||
op.create_foreign_key('harvest_cache_time_entry_fk_client',
|
||||
'harvest_cache_time_entry', 'harvest_cache_client',
|
||||
['client_id'], ['id'])
|
||||
op.create_foreign_key('harvest_cache_time_entry_fk_project',
|
||||
'harvest_cache_time_entry', 'harvest_cache_project',
|
||||
['project_id'], ['id'])
|
||||
op.create_foreign_key('harvest_cache_time_entry_fk_task',
|
||||
'harvest_cache_time_entry', 'harvest_cache_task',
|
||||
['task_id'], ['id'])
|
||||
op.create_unique_constraint('harvest_cache_time_entry_uq_id', 'harvest_cache_time_entry', ['id'])
|
||||
|
||||
|
||||
def downgrade():
|
||||
|
||||
##############################
|
||||
# drop all constraints
|
||||
##############################
|
||||
|
||||
# harvest_cache_time_entry
|
||||
op.drop_constraint('harvest_cache_time_entry_fk_user', 'harvest_cache_time_entry', type_='foreignkey')
|
||||
op.drop_constraint('harvest_cache_time_entry_fk_client', 'harvest_cache_time_entry', type_='foreignkey')
|
||||
op.drop_constraint('harvest_cache_time_entry_fk_project', 'harvest_cache_time_entry', type_='foreignkey')
|
||||
op.drop_constraint('harvest_cache_time_entry_fk_task', 'harvest_cache_time_entry', type_='foreignkey')
|
||||
op.drop_constraint('harvest_cache_time_entry_uq_id', 'harvest_cache_time_entry', type_='unique')
|
||||
|
||||
# harvest_cache_task
|
||||
op.drop_constraint('harvest_cache_task_uq_id', 'harvest_cache_task', type_='unique')
|
||||
|
||||
# harvest_cache_project
|
||||
op.drop_constraint('harvest_cache_project_fk_client', 'harvest_cache_project', type_='foreignkey')
|
||||
op.drop_constraint('harvest_cache_project_uq_id', 'harvest_cache_project', type_='unique')
|
||||
|
||||
# harvest_cache_client
|
||||
op.drop_constraint('harvest_cache_client_uq_id', 'harvest_cache_client', type_='unique')
|
||||
|
||||
# harvest_cache_user
|
||||
op.drop_constraint('harvest_cache_user_fk_person', 'harvest_cache_user', type_='foreignkey')
|
||||
op.drop_constraint('harvest_cache_user_uq_id', 'harvest_cache_user', type_='unique')
|
||||
|
||||
##############################
|
||||
# rename all tables
|
||||
##############################
|
||||
|
||||
op.rename_table('harvest_cache_user', 'harvest_user')
|
||||
op.rename_table('harvest_cache_user_version', 'harvest_user_version')
|
||||
op.rename_table('harvest_cache_client', 'harvest_client')
|
||||
op.rename_table('harvest_cache_client_version', 'harvest_client_version')
|
||||
op.rename_table('harvest_cache_project', 'harvest_project')
|
||||
op.rename_table('harvest_cache_project_version', 'harvest_project_version')
|
||||
op.rename_table('harvest_cache_task', 'harvest_task')
|
||||
op.rename_table('harvest_cache_task_version', 'harvest_task_version')
|
||||
op.rename_table('harvest_cache_time_entry', 'harvest_time_entry')
|
||||
op.rename_table('harvest_cache_time_entry_version', 'harvest_time_entry_version')
|
||||
|
||||
##############################
|
||||
# re-create all constraints
|
||||
##############################
|
||||
|
||||
# harvest_user
|
||||
op.create_foreign_key('harvest_user_fk_person',
|
||||
'harvest_user', 'person',
|
||||
['person_uuid'], ['uuid'])
|
||||
op.create_unique_constraint('harvest_user_uq_id', 'harvest_user', ['id'])
|
||||
|
||||
# harvest_client
|
||||
op.create_unique_constraint('harvest_client_uq_id', 'harvest_client', ['id'])
|
||||
|
||||
# harvest_project
|
||||
op.create_foreign_key('harvest_project_fk_client',
|
||||
'harvest_project', 'harvest_client',
|
||||
['client_id'], ['id'])
|
||||
op.create_unique_constraint('harvest_project_uq_id', 'harvest_project', ['id'])
|
||||
|
||||
# harvest_cache_task
|
||||
op.create_unique_constraint('harvest_task_uq_id', 'harvest_task', ['id'])
|
||||
|
||||
# harvest_time_entry
|
||||
op.create_foreign_key('harvest_time_entry_fk_user',
|
||||
'harvest_time_entry', 'harvest_user',
|
||||
['user_id'], ['id'])
|
||||
op.create_foreign_key('harvest_time_entry_fk_client',
|
||||
'harvest_time_entry', 'harvest_client',
|
||||
['client_id'], ['id'])
|
||||
op.create_foreign_key('harvest_time_entry_fk_project',
|
||||
'harvest_time_entry', 'harvest_project',
|
||||
['project_id'], ['id'])
|
||||
op.create_foreign_key('harvest_time_entry_fk_task',
|
||||
'harvest_time_entry', 'harvest_task',
|
||||
['task_id'], ['id'])
|
||||
op.create_unique_constraint('harvest_time_entry_uq_id', 'harvest_time_entry', ['id'])
|
|
@ -0,0 +1,33 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
"""add project.deleted
|
||||
|
||||
Revision ID: 5505c0e60d28
|
||||
Revises: d59ce24c2f9f
|
||||
Create Date: 2022-01-30 12:08:04.338229
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '5505c0e60d28'
|
||||
down_revision = 'd59ce24c2f9f'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import rattail.db.types
|
||||
|
||||
|
||||
|
||||
def upgrade():
|
||||
|
||||
# harvest_project
|
||||
op.add_column('harvest_project', sa.Column('deleted', sa.Boolean(), nullable=True))
|
||||
op.add_column('harvest_project_version', sa.Column('deleted', sa.Boolean(), autoincrement=False, nullable=True))
|
||||
|
||||
|
||||
def downgrade():
|
||||
|
||||
# harvest_project
|
||||
op.drop_column('harvest_project_version', 'deleted')
|
||||
op.drop_column('harvest_project', 'deleted')
|
|
@ -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')
|
105
rattail_harvest/db/alembic/versions/a1cf300fb371_fix_indeces.py
Normal file
105
rattail_harvest/db/alembic/versions/a1cf300fb371_fix_indeces.py
Normal file
|
@ -0,0 +1,105 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
"""fix indeces
|
||||
|
||||
Revision ID: a1cf300fb371
|
||||
Revises: 53c066772ad5
|
||||
Create Date: 2023-10-23 17:35:15.527740
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'a1cf300fb371'
|
||||
down_revision = '53c066772ad5'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import rattail.db.types
|
||||
|
||||
|
||||
|
||||
def upgrade():
|
||||
|
||||
# harvest_cache_user
|
||||
op.drop_index('ix_harvest_user_version_end_transaction_id', table_name='harvest_cache_user_version')
|
||||
op.drop_index('ix_harvest_user_version_operation_type', table_name='harvest_cache_user_version')
|
||||
op.drop_index('ix_harvest_user_version_transaction_id', table_name='harvest_cache_user_version')
|
||||
op.create_index(op.f('ix_harvest_cache_user_version_end_transaction_id'), 'harvest_cache_user_version', ['end_transaction_id'], unique=False)
|
||||
op.create_index(op.f('ix_harvest_cache_user_version_operation_type'), 'harvest_cache_user_version', ['operation_type'], unique=False)
|
||||
op.create_index(op.f('ix_harvest_cache_user_version_transaction_id'), 'harvest_cache_user_version', ['transaction_id'], unique=False)
|
||||
|
||||
# harvest_cache_client
|
||||
op.drop_index('ix_harvest_client_version_end_transaction_id', table_name='harvest_cache_client_version')
|
||||
op.drop_index('ix_harvest_client_version_operation_type', table_name='harvest_cache_client_version')
|
||||
op.drop_index('ix_harvest_client_version_transaction_id', table_name='harvest_cache_client_version')
|
||||
op.create_index(op.f('ix_harvest_cache_client_version_end_transaction_id'), 'harvest_cache_client_version', ['end_transaction_id'], unique=False)
|
||||
op.create_index(op.f('ix_harvest_cache_client_version_operation_type'), 'harvest_cache_client_version', ['operation_type'], unique=False)
|
||||
op.create_index(op.f('ix_harvest_cache_client_version_transaction_id'), 'harvest_cache_client_version', ['transaction_id'], unique=False)
|
||||
|
||||
# harvest_cache_project
|
||||
op.drop_index('ix_harvest_project_version_end_transaction_id', table_name='harvest_cache_project_version')
|
||||
op.drop_index('ix_harvest_project_version_operation_type', table_name='harvest_cache_project_version')
|
||||
op.drop_index('ix_harvest_project_version_transaction_id', table_name='harvest_cache_project_version')
|
||||
op.create_index(op.f('ix_harvest_cache_project_version_end_transaction_id'), 'harvest_cache_project_version', ['end_transaction_id'], unique=False)
|
||||
op.create_index(op.f('ix_harvest_cache_project_version_operation_type'), 'harvest_cache_project_version', ['operation_type'], unique=False)
|
||||
op.create_index(op.f('ix_harvest_cache_project_version_transaction_id'), 'harvest_cache_project_version', ['transaction_id'], unique=False)
|
||||
|
||||
# harvest_cache_task
|
||||
op.drop_index('ix_harvest_task_version_end_transaction_id', table_name='harvest_cache_task_version')
|
||||
op.drop_index('ix_harvest_task_version_operation_type', table_name='harvest_cache_task_version')
|
||||
op.drop_index('ix_harvest_task_version_transaction_id', table_name='harvest_cache_task_version')
|
||||
op.create_index(op.f('ix_harvest_cache_task_version_end_transaction_id'), 'harvest_cache_task_version', ['end_transaction_id'], unique=False)
|
||||
op.create_index(op.f('ix_harvest_cache_task_version_operation_type'), 'harvest_cache_task_version', ['operation_type'], unique=False)
|
||||
op.create_index(op.f('ix_harvest_cache_task_version_transaction_id'), 'harvest_cache_task_version', ['transaction_id'], unique=False)
|
||||
|
||||
# harvest_cache_time_entry
|
||||
op.drop_index('ix_harvest_time_entry_version_end_transaction_id', table_name='harvest_cache_time_entry_version')
|
||||
op.drop_index('ix_harvest_time_entry_version_operation_type', table_name='harvest_cache_time_entry_version')
|
||||
op.drop_index('ix_harvest_time_entry_version_transaction_id', table_name='harvest_cache_time_entry_version')
|
||||
op.create_index(op.f('ix_harvest_cache_time_entry_version_end_transaction_id'), 'harvest_cache_time_entry_version', ['end_transaction_id'], unique=False)
|
||||
op.create_index(op.f('ix_harvest_cache_time_entry_version_operation_type'), 'harvest_cache_time_entry_version', ['operation_type'], unique=False)
|
||||
op.create_index(op.f('ix_harvest_cache_time_entry_version_transaction_id'), 'harvest_cache_time_entry_version', ['transaction_id'], unique=False)
|
||||
|
||||
|
||||
def downgrade():
|
||||
|
||||
# harvest_cache_time_entry
|
||||
op.drop_index(op.f('ix_harvest_cache_time_entry_version_transaction_id'), table_name='harvest_cache_time_entry_version')
|
||||
op.drop_index(op.f('ix_harvest_cache_time_entry_version_operation_type'), table_name='harvest_cache_time_entry_version')
|
||||
op.drop_index(op.f('ix_harvest_cache_time_entry_version_end_transaction_id'), table_name='harvest_cache_time_entry_version')
|
||||
op.create_index('ix_harvest_time_entry_version_transaction_id', 'harvest_cache_time_entry_version', ['transaction_id'], unique=False)
|
||||
op.create_index('ix_harvest_time_entry_version_operation_type', 'harvest_cache_time_entry_version', ['operation_type'], unique=False)
|
||||
op.create_index('ix_harvest_time_entry_version_end_transaction_id', 'harvest_cache_time_entry_version', ['end_transaction_id'], unique=False)
|
||||
|
||||
# harvest_cache_task
|
||||
op.drop_index(op.f('ix_harvest_cache_task_version_transaction_id'), table_name='harvest_cache_task_version')
|
||||
op.drop_index(op.f('ix_harvest_cache_task_version_operation_type'), table_name='harvest_cache_task_version')
|
||||
op.drop_index(op.f('ix_harvest_cache_task_version_end_transaction_id'), table_name='harvest_cache_task_version')
|
||||
op.create_index('ix_harvest_task_version_transaction_id', 'harvest_cache_task_version', ['transaction_id'], unique=False)
|
||||
op.create_index('ix_harvest_task_version_operation_type', 'harvest_cache_task_version', ['operation_type'], unique=False)
|
||||
op.create_index('ix_harvest_task_version_end_transaction_id', 'harvest_cache_task_version', ['end_transaction_id'], unique=False)
|
||||
|
||||
# harvest_cache_project
|
||||
op.drop_index(op.f('ix_harvest_cache_project_version_transaction_id'), table_name='harvest_cache_project_version')
|
||||
op.drop_index(op.f('ix_harvest_cache_project_version_operation_type'), table_name='harvest_cache_project_version')
|
||||
op.drop_index(op.f('ix_harvest_cache_project_version_end_transaction_id'), table_name='harvest_cache_project_version')
|
||||
op.create_index('ix_harvest_project_version_transaction_id', 'harvest_cache_project_version', ['transaction_id'], unique=False)
|
||||
op.create_index('ix_harvest_project_version_operation_type', 'harvest_cache_project_version', ['operation_type'], unique=False)
|
||||
op.create_index('ix_harvest_project_version_end_transaction_id', 'harvest_cache_project_version', ['end_transaction_id'], unique=False)
|
||||
|
||||
# harvest_cache_client
|
||||
op.drop_index(op.f('ix_harvest_cache_client_version_transaction_id'), table_name='harvest_cache_client_version')
|
||||
op.drop_index(op.f('ix_harvest_cache_client_version_operation_type'), table_name='harvest_cache_client_version')
|
||||
op.drop_index(op.f('ix_harvest_cache_client_version_end_transaction_id'), table_name='harvest_cache_client_version')
|
||||
op.create_index('ix_harvest_client_version_transaction_id', 'harvest_cache_client_version', ['transaction_id'], unique=False)
|
||||
op.create_index('ix_harvest_client_version_operation_type', 'harvest_cache_client_version', ['operation_type'], unique=False)
|
||||
op.create_index('ix_harvest_client_version_end_transaction_id', 'harvest_cache_client_version', ['end_transaction_id'], unique=False)
|
||||
|
||||
# harvest_cache_user
|
||||
op.drop_index(op.f('ix_harvest_cache_user_version_transaction_id'), table_name='harvest_cache_user_version')
|
||||
op.drop_index(op.f('ix_harvest_cache_user_version_operation_type'), table_name='harvest_cache_user_version')
|
||||
op.drop_index(op.f('ix_harvest_cache_user_version_end_transaction_id'), table_name='harvest_cache_user_version')
|
||||
op.create_index('ix_harvest_user_version_transaction_id', 'harvest_cache_user_version', ['transaction_id'], unique=False)
|
||||
op.create_index('ix_harvest_user_version_operation_type', 'harvest_cache_user_version', ['operation_type'], unique=False)
|
||||
op.create_index('ix_harvest_user_version_end_transaction_id', 'harvest_cache_user_version', ['end_transaction_id'], unique=False)
|
|
@ -0,0 +1,89 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
"""grow id fields
|
||||
|
||||
Revision ID: f2a1650e7fbc
|
||||
Revises: 6bc1cb21d920
|
||||
Create Date: 2023-08-08 10:53:56.013211
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'f2a1650e7fbc'
|
||||
down_revision = '6bc1cb21d920'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import rattail.db.types
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
|
||||
def upgrade():
|
||||
|
||||
# harvest_user
|
||||
op.alter_column('harvest_user', 'id', type_=sa.BigInteger())
|
||||
op.alter_column('harvest_user_version', 'id', type_=sa.BigInteger())
|
||||
|
||||
# harvest_client
|
||||
op.alter_column('harvest_client', 'id', type_=sa.BigInteger())
|
||||
op.alter_column('harvest_client_version', 'id', type_=sa.BigInteger())
|
||||
|
||||
# harvest_project
|
||||
op.alter_column('harvest_project', 'id', type_=sa.BigInteger())
|
||||
op.alter_column('harvest_project', 'client_id', type_=sa.BigInteger())
|
||||
op.alter_column('harvest_project_version', 'id', type_=sa.BigInteger())
|
||||
op.alter_column('harvest_project_version', 'client_id', type_=sa.BigInteger())
|
||||
|
||||
# harvest_task
|
||||
op.alter_column('harvest_task', 'id', type_=sa.BigInteger())
|
||||
op.alter_column('harvest_task_version', 'id', type_=sa.BigInteger())
|
||||
|
||||
# harvest_time_entry
|
||||
op.alter_column('harvest_time_entry', 'id', type_=sa.BigInteger())
|
||||
op.alter_column('harvest_time_entry', 'user_id', type_=sa.BigInteger())
|
||||
op.alter_column('harvest_time_entry', 'client_id', type_=sa.BigInteger())
|
||||
op.alter_column('harvest_time_entry', 'project_id', type_=sa.BigInteger())
|
||||
op.alter_column('harvest_time_entry', 'task_id', type_=sa.BigInteger())
|
||||
op.alter_column('harvest_time_entry', 'invoice_id', type_=sa.BigInteger())
|
||||
op.alter_column('harvest_time_entry_version', 'id', type_=sa.BigInteger())
|
||||
op.alter_column('harvest_time_entry_version', 'user_id', type_=sa.BigInteger())
|
||||
op.alter_column('harvest_time_entry_version', 'client_id', type_=sa.BigInteger())
|
||||
op.alter_column('harvest_time_entry_version', 'project_id', type_=sa.BigInteger())
|
||||
op.alter_column('harvest_time_entry_version', 'task_id', type_=sa.BigInteger())
|
||||
op.alter_column('harvest_time_entry_version', 'invoice_id', type_=sa.BigInteger())
|
||||
|
||||
|
||||
def downgrade():
|
||||
|
||||
# harvest_time_entry
|
||||
op.alter_column('harvest_time_entry_version', 'id', type_=sa.Integer())
|
||||
op.alter_column('harvest_time_entry_version', 'user_id', type_=sa.Integer())
|
||||
op.alter_column('harvest_time_entry_version', 'client_id', type_=sa.Integer())
|
||||
op.alter_column('harvest_time_entry_version', 'project_id', type_=sa.Integer())
|
||||
op.alter_column('harvest_time_entry_version', 'task_id', type_=sa.Integer())
|
||||
op.alter_column('harvest_time_entry_version', 'invoice_id', type_=sa.Integer())
|
||||
op.alter_column('harvest_time_entry', 'id', type_=sa.Integer())
|
||||
op.alter_column('harvest_time_entry', 'user_id', type_=sa.Integer())
|
||||
op.alter_column('harvest_time_entry', 'client_id', type_=sa.Integer())
|
||||
op.alter_column('harvest_time_entry', 'project_id', type_=sa.Integer())
|
||||
op.alter_column('harvest_time_entry', 'task_id', type_=sa.Integer())
|
||||
op.alter_column('harvest_time_entry', 'invoice_id', type_=sa.Integer())
|
||||
|
||||
# harvest_task
|
||||
op.alter_column('harvest_task_version', 'id', type_=sa.Integer())
|
||||
op.alter_column('harvest_task', 'id', type_=sa.Integer())
|
||||
|
||||
# harvest_project
|
||||
op.alter_column('harvest_project_version', 'id', type_=sa.Integer())
|
||||
op.alter_column('harvest_project_version', 'client_id', type_=sa.Integer())
|
||||
op.alter_column('harvest_project', 'id', type_=sa.Integer())
|
||||
op.alter_column('harvest_project', 'client_id', type_=sa.Integer())
|
||||
|
||||
# harvest_client
|
||||
op.alter_column('harvest_client_version', 'id', type_=sa.Integer())
|
||||
op.alter_column('harvest_client', 'id', type_=sa.Integer())
|
||||
|
||||
# harvest_user
|
||||
op.alter_column('harvest_user_version', 'id', type_=sa.Integer())
|
||||
op.alter_column('harvest_user', 'id', type_=sa.Integer())
|
|
@ -2,7 +2,7 @@
|
|||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2022 Lance Edgar
|
||||
# Copyright © 2010-2023 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
|
@ -24,5 +24,6 @@
|
|||
Harvest integration data models
|
||||
"""
|
||||
|
||||
from .harvest import (HarvestUser, HarvestClient, HarvestProject,
|
||||
HarvestTask, HarvestTimeEntry)
|
||||
from .harvest import (HarvestCacheUser, HarvestCacheClient,
|
||||
HarvestCacheProject, HarvestCacheTask,
|
||||
HarvestCacheTimeEntry)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2022 Lance Edgar
|
||||
# Copyright © 2010-2023 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
|
@ -24,6 +24,8 @@
|
|||
Harvest "cache" data models
|
||||
"""
|
||||
|
||||
import warnings
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import orm
|
||||
|
||||
|
@ -31,21 +33,23 @@ from rattail.db import model
|
|||
from rattail.db.util import normalize_full_name
|
||||
|
||||
|
||||
class HarvestUser(model.Base):
|
||||
class HarvestCacheUser(model.Base):
|
||||
"""
|
||||
Represents a user record in Harvest.
|
||||
|
||||
https://help.getharvest.com/api-v2/users-api/users/users/#the-user-object
|
||||
"""
|
||||
__tablename__ = 'harvest_user'
|
||||
__tablename__ = 'harvest_cache_user'
|
||||
__table_args__ = (
|
||||
sa.UniqueConstraint('id', name='harvest_user_uq_id'),
|
||||
sa.ForeignKeyConstraint(['person_uuid'], ['person.uuid'],
|
||||
name='harvest_cache_user_fk_person'),
|
||||
sa.UniqueConstraint('id', name='harvest_cache_user_uq_id'),
|
||||
)
|
||||
__versioned__ = {}
|
||||
|
||||
uuid = model.uuid_column()
|
||||
|
||||
id = sa.Column(sa.Integer(), nullable=False)
|
||||
id = sa.Column(sa.BigInteger(), nullable=False)
|
||||
|
||||
first_name = sa.Column(sa.String(length=255), nullable=True)
|
||||
|
||||
|
@ -90,25 +94,38 @@ 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)
|
||||
|
||||
|
||||
class HarvestClient(model.Base):
|
||||
class HarvestCacheClient(model.Base):
|
||||
"""
|
||||
Represents a client record in Harvest.
|
||||
|
||||
https://help.getharvest.com/api-v2/clients-api/clients/clients/#the-client-object
|
||||
"""
|
||||
__tablename__ = 'harvest_client'
|
||||
__tablename__ = 'harvest_cache_client'
|
||||
__table_args__ = (
|
||||
sa.UniqueConstraint('id', name='harvest_client_uq_id'),
|
||||
sa.UniqueConstraint('id', name='harvest_cache_client_uq_id'),
|
||||
)
|
||||
__versioned__ = {}
|
||||
|
||||
uuid = model.uuid_column()
|
||||
|
||||
id = sa.Column(sa.Integer(), nullable=False)
|
||||
id = sa.Column(sa.BigInteger(), nullable=False)
|
||||
|
||||
name = sa.Column(sa.String(length=255), nullable=True)
|
||||
|
||||
|
@ -126,25 +143,26 @@ class HarvestClient(model.Base):
|
|||
return self.name or ''
|
||||
|
||||
|
||||
class HarvestProject(model.Base):
|
||||
class HarvestCacheProject(model.Base):
|
||||
"""
|
||||
Represents a project record in Harvest.
|
||||
|
||||
https://help.getharvest.com/api-v2/projects-api/projects/projects/#the-project-object
|
||||
"""
|
||||
__tablename__ = 'harvest_project'
|
||||
__tablename__ = 'harvest_cache_project'
|
||||
__table_args__ = (
|
||||
sa.UniqueConstraint('id', name='harvest_project_uq_id'),
|
||||
sa.ForeignKeyConstraint(['client_id'], ['harvest_client.id'], name='harvest_project_fk_client'),
|
||||
sa.UniqueConstraint('id', name='harvest_cache_project_uq_id'),
|
||||
sa.ForeignKeyConstraint(['client_id'], ['harvest_cache_client.id'],
|
||||
name='harvest_cache_project_fk_client'),
|
||||
)
|
||||
__versioned__ = {'exclude': ['over_budget_notification_date']}
|
||||
|
||||
uuid = model.uuid_column()
|
||||
|
||||
id = sa.Column(sa.Integer(), nullable=False)
|
||||
id = sa.Column(sa.BigInteger(), nullable=False)
|
||||
|
||||
client_id = sa.Column(sa.Integer(), nullable=True) # TODO: should not allow null?
|
||||
client = orm.relationship(HarvestClient, backref=orm.backref('projects'))
|
||||
client_id = sa.Column(sa.BigInteger(), nullable=True) # TODO: should not allow null?
|
||||
client = orm.relationship(HarvestCacheClient, backref=orm.backref('projects'))
|
||||
|
||||
name = sa.Column(sa.String(length=255), nullable=True)
|
||||
|
||||
|
@ -190,25 +208,29 @@ class HarvestProject(model.Base):
|
|||
|
||||
updated_at = sa.Column(sa.DateTime(), nullable=True)
|
||||
|
||||
deleted = sa.Column(sa.Boolean(), nullable=True, doc="""
|
||||
Flag indicating the record has been deleted in Harvest.
|
||||
""")
|
||||
|
||||
def __str__(self):
|
||||
return self.name or ''
|
||||
|
||||
|
||||
class HarvestTask(model.Base):
|
||||
class HarvestCacheTask(model.Base):
|
||||
"""
|
||||
Represents a task record in Harvest.
|
||||
|
||||
https://help.getharvest.com/api-v2/tasks-api/tasks/tasks/#the-task-object
|
||||
"""
|
||||
__tablename__ = 'harvest_task'
|
||||
__tablename__ = 'harvest_cache_task'
|
||||
__table_args__ = (
|
||||
sa.UniqueConstraint('id', name='harvest_task_uq_id'),
|
||||
sa.UniqueConstraint('id', name='harvest_cache_task_uq_id'),
|
||||
)
|
||||
__versioned__ = {}
|
||||
|
||||
uuid = model.uuid_column()
|
||||
|
||||
id = sa.Column(sa.Integer(), nullable=False)
|
||||
id = sa.Column(sa.BigInteger(), nullable=False)
|
||||
|
||||
name = sa.Column(sa.String(length=255), nullable=True)
|
||||
|
||||
|
@ -228,42 +250,46 @@ class HarvestTask(model.Base):
|
|||
return self.name or ''
|
||||
|
||||
|
||||
class HarvestTimeEntry(model.Base):
|
||||
class HarvestCacheTimeEntry(model.Base):
|
||||
"""
|
||||
Represents a time entry record in Harvest.
|
||||
|
||||
https://help.getharvest.com/api-v2/timesheets-api/timesheets/time-entries/#the-time-entry-object
|
||||
"""
|
||||
__tablename__ = 'harvest_time_entry'
|
||||
__tablename__ = 'harvest_cache_time_entry'
|
||||
__table_args__ = (
|
||||
sa.UniqueConstraint('id', name='harvest_time_entry_uq_id'),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['harvest_user.id'], name='harvest_time_entry_fk_user'),
|
||||
sa.ForeignKeyConstraint(['client_id'], ['harvest_client.id'], name='harvest_time_entry_fk_client'),
|
||||
sa.ForeignKeyConstraint(['project_id'], ['harvest_project.id'], name='harvest_time_entry_fk_project'),
|
||||
sa.ForeignKeyConstraint(['task_id'], ['harvest_task.id'], name='harvest_time_entry_fk_task'),
|
||||
sa.UniqueConstraint('id', name='harvest_cache_time_entry_uq_id'),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['harvest_cache_user.id'],
|
||||
name='harvest_cache_time_entry_fk_user'),
|
||||
sa.ForeignKeyConstraint(['client_id'], ['harvest_cache_client.id'],
|
||||
name='harvest_cache_time_entry_fk_client'),
|
||||
sa.ForeignKeyConstraint(['project_id'], ['harvest_cache_project.id'],
|
||||
name='harvest_cache_time_entry_fk_project'),
|
||||
sa.ForeignKeyConstraint(['task_id'], ['harvest_cache_task.id'],
|
||||
name='harvest_cache_time_entry_fk_task'),
|
||||
)
|
||||
__versioned__ = {}
|
||||
model_title_plural = "Harvest Time Entries"
|
||||
|
||||
uuid = model.uuid_column()
|
||||
|
||||
id = sa.Column(sa.Integer(), nullable=False)
|
||||
id = sa.Column(sa.BigInteger(), nullable=False)
|
||||
|
||||
spent_date = sa.Column(sa.Date(), nullable=True)
|
||||
|
||||
user_id = sa.Column(sa.Integer(), nullable=True)
|
||||
user = orm.relationship(HarvestUser, backref=orm.backref('time_entries'))
|
||||
user_id = sa.Column(sa.BigInteger(), nullable=True)
|
||||
user = orm.relationship(HarvestCacheUser, backref=orm.backref('time_entries'))
|
||||
|
||||
client_id = sa.Column(sa.Integer(), nullable=True)
|
||||
client = orm.relationship(HarvestClient, backref=orm.backref('time_entries'))
|
||||
client_id = sa.Column(sa.BigInteger(), nullable=True)
|
||||
client = orm.relationship(HarvestCacheClient, backref=orm.backref('time_entries'))
|
||||
|
||||
project_id = sa.Column(sa.Integer(), nullable=True)
|
||||
project = orm.relationship(HarvestProject, backref=orm.backref('time_entries'))
|
||||
project_id = sa.Column(sa.BigInteger(), nullable=True)
|
||||
project = orm.relationship(HarvestCacheProject, backref=orm.backref('time_entries'))
|
||||
|
||||
task_id = sa.Column(sa.Integer(), nullable=True)
|
||||
task = orm.relationship(HarvestTask, backref=orm.backref('time_entries'))
|
||||
task_id = sa.Column(sa.BigInteger(), nullable=True)
|
||||
task = orm.relationship(HarvestCacheTask, backref=orm.backref('time_entries'))
|
||||
|
||||
invoice_id = sa.Column(sa.Integer(), nullable=True)
|
||||
invoice_id = sa.Column(sa.BigInteger(), nullable=True)
|
||||
|
||||
hours = sa.Column(sa.Numeric(precision=6, scale=2), nullable=True)
|
||||
|
||||
|
|
31
rattail_harvest/harvest/config.py
Normal file
31
rattail_harvest/harvest/config.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
# -*- 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 config
|
||||
"""
|
||||
|
||||
|
||||
def get_harvest_url(config):
|
||||
url = config.get('harvest', 'url')
|
||||
if url:
|
||||
return url.rstrip('/')
|
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
|
169
rattail_harvest/harvest/importing/model.py
Normal file
169
rattail_harvest/harvest/importing/model.py
Normal file
|
@ -0,0 +1,169 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2023 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().setup()
|
||||
self.setup_webapi()
|
||||
|
||||
def datasync_setup(self):
|
||||
super().datasync_setup()
|
||||
self.setup_webapi()
|
||||
|
||||
def setup_webapi(self):
|
||||
self.webapi = make_harvest_webapi(self.config)
|
||||
|
||||
|
||||
class TimeEntryImporter(ToHarvest):
|
||||
"""
|
||||
Harvest time entry data importer.
|
||||
"""
|
||||
model_name = 'TimeEntry'
|
||||
key = 'id'
|
||||
supported_fields = [
|
||||
'id',
|
||||
'user_id',
|
||||
'client_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 = {}
|
||||
|
||||
# TODO: we try to avoid entries w/ timer still running here,
|
||||
# but for some reason they still come back, so double-check
|
||||
kw = {'is_running': False}
|
||||
if self.start_date:
|
||||
kw['from'] = self.start_date
|
||||
if self.end_date:
|
||||
kw['to'] = self.end_date
|
||||
entries = self.webapi.get_time_entries(**kw)
|
||||
for entry in entries:
|
||||
# double-check here
|
||||
if not entry['is_running']:
|
||||
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 get_single_local_object(self, key):
|
||||
assert len(self.key) == 1 and self.key[0] == 'id'
|
||||
entry_id = key[0]
|
||||
if entry_id > 0:
|
||||
return self.webapi.get_time_entry(entry_id)
|
||||
|
||||
def normalize_local_object(self, entry):
|
||||
data = {
|
||||
'id': entry['id'],
|
||||
'client_id': entry['client']['id'],
|
||||
'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 get_next_harvest_id(self):
|
||||
if hasattr(self, 'next_harvest_id'):
|
||||
next_id = self.next_harvest_id
|
||||
else:
|
||||
next_id = 1
|
||||
self.next_harvest_id = next_id + 1
|
||||
return -next_id
|
||||
|
||||
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']}
|
||||
if 'client_id' in self.fields:
|
||||
result['client'] = {'id': result['client_id']}
|
||||
result['project'] = {'id': result['project_id']}
|
||||
result['task'] = {'id': result['task_id']}
|
||||
return result
|
||||
|
||||
kwargs = {
|
||||
'client_id': host_data['client_id'],
|
||||
'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, entry, host_data, local_data=None, all_fields=False):
|
||||
if self.dry_run:
|
||||
return entry
|
||||
|
||||
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'],
|
||||
}
|
||||
|
||||
return self.webapi.update_time_entry(entry['id'], **kwargs)
|
||||
|
||||
def delete_object(self, entry):
|
||||
if self.dry_run:
|
||||
return True
|
||||
|
||||
self.webapi.delete_time_entry(entry['id'])
|
||||
return True
|
|
@ -58,6 +58,12 @@ class HarvestWebAPI(object):
|
|||
elif request_method == 'POST':
|
||||
response = requests.post('{}/{}'.format(self.base_url, api_method),
|
||||
headers=headers, params=params)
|
||||
elif request_method == 'PATCH':
|
||||
response = requests.patch('{}/{}'.format(self.base_url, api_method),
|
||||
headers=headers, params=params)
|
||||
elif request_method == 'DELETE':
|
||||
response = requests.delete('{}/{}'.format(self.base_url, api_method),
|
||||
headers=headers, params=params)
|
||||
else:
|
||||
raise NotImplementedError("unknown request method: {}".format(
|
||||
request_method))
|
||||
|
@ -76,6 +82,18 @@ class HarvestWebAPI(object):
|
|||
"""
|
||||
return self._request('POST', api_method, params=params)
|
||||
|
||||
def patch(self, api_method, params=None):
|
||||
"""
|
||||
Perform a PATCH request for the given API method, and return the response.
|
||||
"""
|
||||
return self._request('PATCH', api_method, params=params)
|
||||
|
||||
def delete(self, api_method, params=None):
|
||||
"""
|
||||
Perform a DELETE request for the given API method, and return the response.
|
||||
"""
|
||||
return self._request('DELETE', api_method, params=params)
|
||||
|
||||
def get_company(self):
|
||||
"""
|
||||
Retrieves the company for the currently authenticated user.
|
||||
|
@ -113,7 +131,17 @@ class HarvestWebAPI(object):
|
|||
https://help.getharvest.com/api-v2/projects-api/projects/projects/#list-all-projects
|
||||
"""
|
||||
response = self.get('/projects', params=kwargs)
|
||||
return response.json()
|
||||
data = response.json()
|
||||
projects = data['projects']
|
||||
while data['next_page']:
|
||||
|
||||
kw = dict(kwargs)
|
||||
kw['page'] = data['next_page']
|
||||
response = self.get('/projects', params=kw)
|
||||
data = response.json()
|
||||
projects.extend(data['projects'])
|
||||
|
||||
return projects
|
||||
|
||||
def get_tasks(self, **kwargs):
|
||||
"""
|
||||
|
@ -151,10 +179,15 @@ class HarvestWebAPI(object):
|
|||
|
||||
https://help.getharvest.com/api-v2/timesheets-api/timesheets/time-entries/#retrieve-a-time-entry
|
||||
"""
|
||||
response = self.get('/time_entries/{}'.format(time_entry_id))
|
||||
return response.json()
|
||||
try:
|
||||
response = self.get('/time_entries/{}'.format(time_entry_id))
|
||||
except requests.exceptions.HTTPError as error:
|
||||
if error.response.status_code != 404:
|
||||
raise
|
||||
else:
|
||||
return response.json()
|
||||
|
||||
def put_time_entry(self, **kwargs):
|
||||
def create_time_entry(self, **kwargs):
|
||||
"""
|
||||
Create a new time entry. All kwargs are passed on as POST parameters.
|
||||
|
||||
|
@ -167,3 +200,41 @@ class HarvestWebAPI(object):
|
|||
raise ValueError("must provide all of: {}".format(', '.join(required)))
|
||||
response = self.post('/time_entries', params=kwargs)
|
||||
return response.json()
|
||||
|
||||
# TODO: deprecate / remove this
|
||||
put_time_entry = create_time_entry
|
||||
|
||||
def stop_time_entry(self, time_entry_id):
|
||||
"""
|
||||
Stop a running time entry.
|
||||
|
||||
https://help.getharvest.com/api-v2/timesheets-api/timesheets/time-entries/#stop-a-running-time-entry
|
||||
"""
|
||||
response = self.patch('/time_entries/{}/stop'.format(time_entry_id))
|
||||
return response.json()
|
||||
|
||||
def update_time_entry(self, time_entry_id, **kwargs):
|
||||
"""
|
||||
Update a time entry.
|
||||
|
||||
https://help.getharvest.com/api-v2/timesheets-api/timesheets/time-entries/#update-a-time-entry
|
||||
"""
|
||||
response = self.patch('/time_entries/{}'.format(time_entry_id), params=kwargs)
|
||||
return response.json()
|
||||
|
||||
def delete_time_entry(self, time_entry_id, **kwargs):
|
||||
"""
|
||||
Delete a time entry.
|
||||
|
||||
https://help.getharvest.com/api-v2/timesheets-api/timesheets/time-entries/#delete-a-time-entry
|
||||
"""
|
||||
self.delete('/time_entries/{}'.format(time_entry_id), params=kwargs)
|
||||
|
||||
|
||||
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)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2022 Lance Edgar
|
||||
# Copyright © 2010-2023 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
|
@ -27,11 +27,13 @@ Harvest -> Rattail "cache" data import
|
|||
import datetime
|
||||
import decimal
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
|
||||
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__)
|
||||
|
@ -47,11 +49,11 @@ class FromHarvestToRattail(importing.ToRattailHandler):
|
|||
|
||||
def get_importers(self):
|
||||
importers = OrderedDict()
|
||||
importers['HarvestUser'] = HarvestUserImporter
|
||||
importers['HarvestClient'] = HarvestClientImporter
|
||||
importers['HarvestProject'] = HarvestProjectImporter
|
||||
importers['HarvestTask'] = HarvestTaskImporter
|
||||
importers['HarvestTimeEntry'] = HarvestTimeEntryImporter
|
||||
importers['HarvestCacheUser'] = HarvestCacheUserImporter
|
||||
importers['HarvestCacheClient'] = HarvestCacheClientImporter
|
||||
importers['HarvestCacheProject'] = HarvestCacheProjectImporter
|
||||
importers['HarvestCacheTask'] = HarvestCacheTaskImporter
|
||||
importers['HarvestCacheTimeEntry'] = HarvestCacheTimeEntryImporter
|
||||
return importers
|
||||
|
||||
|
||||
|
@ -69,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
|
||||
|
@ -94,14 +90,17 @@ class FromHarvest(importing.Importer):
|
|||
return data
|
||||
|
||||
|
||||
class HarvestUserImporter(FromHarvest, rattail_harvest_importing.model.HarvestUserImporter):
|
||||
class HarvestCacheUserImporter(FromHarvest, rattail_harvest_importing.model.HarvestCacheUserImporter):
|
||||
"""
|
||||
Import user data from Harvest
|
||||
"""
|
||||
|
||||
@property
|
||||
def supported_fields(self):
|
||||
fields = list(super(HarvestUserImporter, self).supported_fields)
|
||||
fields = list(super().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')
|
||||
|
@ -111,8 +110,28 @@ class HarvestUserImporter(FromHarvest, rattail_harvest_importing.model.HarvestUs
|
|||
def get_host_objects(self):
|
||||
return self.webapi.get_users()['users']
|
||||
|
||||
def normalize_host_object(self, user):
|
||||
data = super().normalize_host_object(user)
|
||||
if data:
|
||||
|
||||
class HarvestClientImporter(FromHarvest, rattail_harvest_importing.model.HarvestClientImporter):
|
||||
# TODO: for some reason the API used to include the these
|
||||
# fields, but no longer does as of 2022-11-11, so null is
|
||||
# kinda the only thing that makes sense now. if possible,
|
||||
# should figure out "what changed" at Harvest, but maybe
|
||||
# these fields should just be removed from our cache
|
||||
# schema?
|
||||
data.setdefault('is_admin', None)
|
||||
data.setdefault('is_project_manager', None)
|
||||
data.setdefault('can_see_rates', None)
|
||||
data.setdefault('can_create_invoices', None)
|
||||
|
||||
if data['telephone'] == '':
|
||||
data['telephone'] = None
|
||||
|
||||
return data
|
||||
|
||||
|
||||
class HarvestCacheClientImporter(FromHarvest, rattail_harvest_importing.model.HarvestCacheClientImporter):
|
||||
"""
|
||||
Import client data from Harvest
|
||||
"""
|
||||
|
@ -121,16 +140,32 @@ class HarvestClientImporter(FromHarvest, rattail_harvest_importing.model.Harvest
|
|||
return self.webapi.get_clients()['clients']
|
||||
|
||||
|
||||
class HarvestProjectImporter(FromHarvest, rattail_harvest_importing.model.HarvestProjectImporter):
|
||||
class HarvestCacheProjectImporter(FromHarvest, rattail_harvest_importing.model.HarvestCacheProjectImporter):
|
||||
"""
|
||||
Import project data from Harvest
|
||||
"""
|
||||
|
||||
@property
|
||||
def supported_fields(self):
|
||||
fields = list(super().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.HarvestCacheProject)\
|
||||
.filter(sa.or_(
|
||||
model.HarvestCacheProject.deleted == False,
|
||||
model.HarvestCacheProject.deleted == None))
|
||||
|
||||
def get_host_objects(self):
|
||||
return self.webapi.get_projects()['projects']
|
||||
return self.webapi.get_projects()
|
||||
|
||||
def normalize_host_object(self, project):
|
||||
data = super(HarvestProjectImporter, self).normalize_host_object(project)
|
||||
data = super().normalize_host_object(project)
|
||||
if not data:
|
||||
return
|
||||
|
||||
|
@ -168,8 +203,15 @@ class HarvestProjectImporter(FromHarvest, rattail_harvest_importing.model.Harves
|
|||
|
||||
return data
|
||||
|
||||
def can_delete_object(self, project, data):
|
||||
return not project.deleted
|
||||
|
||||
class HarvestTaskImporter(FromHarvest, rattail_harvest_importing.model.HarvestTaskImporter):
|
||||
def delete_object(self, project):
|
||||
project.deleted = True
|
||||
return True
|
||||
|
||||
|
||||
class HarvestCacheTaskImporter(FromHarvest, rattail_harvest_importing.model.HarvestCacheTaskImporter):
|
||||
"""
|
||||
Import task data from Harvest
|
||||
"""
|
||||
|
@ -178,40 +220,49 @@ class HarvestTaskImporter(FromHarvest, rattail_harvest_importing.model.HarvestTa
|
|||
return self.webapi.get_tasks()['tasks']
|
||||
|
||||
|
||||
class HarvestTimeEntryImporter(FromHarvest, rattail_harvest_importing.model.HarvestTimeEntryImporter):
|
||||
class HarvestCacheTimeEntryImporter(FromHarvest, rattail_harvest_importing.model.HarvestCacheTimeEntryImporter):
|
||||
"""
|
||||
Import time entry data from Harvest
|
||||
"""
|
||||
|
||||
def setup(self):
|
||||
super(HarvestTimeEntryImporter, self).setup()
|
||||
model = self.model
|
||||
|
||||
self.harvest_projects_by_id = self.app.cache_model(self.session,
|
||||
model.HarvestProject,
|
||||
key='id')
|
||||
|
||||
def get_host_objects(self):
|
||||
return self.webapi.get_time_entries(**{'from': self.start_date,
|
||||
'to': self.end_date})
|
||||
kw = {}
|
||||
if self.start_date:
|
||||
kw['from'] = self.start_date
|
||||
if self.end_date:
|
||||
kw['to'] = self.end_date
|
||||
return self.webapi.get_time_entries(**kw)
|
||||
|
||||
def get_single_host_object(self, key):
|
||||
assert len(self.key) == 1 and self.key[0] == 'id'
|
||||
entry_id = key[0]
|
||||
return self.webapi.get_time_entry(entry_id)
|
||||
|
||||
def normalize_host_object(self, entry):
|
||||
data = super(HarvestTimeEntryImporter, self).normalize_host_object(entry)
|
||||
data = super().normalize_host_object(entry)
|
||||
if not data:
|
||||
return
|
||||
|
||||
if entry['is_running']:
|
||||
log.debug("Harvest time entry is still running: %s", entry)
|
||||
return
|
||||
|
||||
data['user_id'] = entry['user']['id']
|
||||
data['client_id'] = entry['client']['id']
|
||||
|
||||
data['project_id'] = entry['project']['id']
|
||||
if data['project_id'] not in self.harvest_projects_by_id:
|
||||
log.warning("time entry references non-existent project id %s: %s",
|
||||
data['project_id'], entry)
|
||||
data['project_id'] = None
|
||||
|
||||
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:
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2022 Lance Edgar
|
||||
# Copyright © 2010-2023 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
|
@ -24,30 +24,86 @@
|
|||
rattail-harvest model importers
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from rattail.importing.model import ToRattail
|
||||
from rattail_harvest.db import model
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
##############################
|
||||
# harvest cache models
|
||||
##############################
|
||||
|
||||
class HarvestUserImporter(ToRattail):
|
||||
model_class = model.HarvestUser
|
||||
class HarvestCacheUserImporter(ToRattail):
|
||||
model_class = model.HarvestCacheUser
|
||||
|
||||
class HarvestClientImporter(ToRattail):
|
||||
model_class = model.HarvestClient
|
||||
class HarvestCacheClientImporter(ToRattail):
|
||||
model_class = model.HarvestCacheClient
|
||||
|
||||
class HarvestProjectImporter(ToRattail):
|
||||
model_class = model.HarvestProject
|
||||
class HarvestCacheProjectImporter(ToRattail):
|
||||
model_class = model.HarvestCacheProject
|
||||
|
||||
class HarvestTaskImporter(ToRattail):
|
||||
model_class = model.HarvestTask
|
||||
class HarvestCacheTaskImporter(ToRattail):
|
||||
model_class = model.HarvestCacheTask
|
||||
|
||||
class HarvestTimeEntryImporter(ToRattail):
|
||||
model_class = model.HarvestTimeEntry
|
||||
class HarvestCacheTimeEntryImporter(ToRattail):
|
||||
model_class = model.HarvestCacheTimeEntry
|
||||
|
||||
# flags to auto-create records for "unknown" references
|
||||
auto_create_unknown_project = True
|
||||
|
||||
# flags to log warning vs. debug for "unknown" references
|
||||
warn_for_unknown_project = True
|
||||
|
||||
def setup(self):
|
||||
super().setup()
|
||||
model = self.model
|
||||
|
||||
if 'project_id' in self.fields:
|
||||
self.harvest_projects_by_id = self.app.cache_model(
|
||||
self.session, model.HarvestCacheProject, key='id')
|
||||
|
||||
def cache_query(self):
|
||||
query = super(HarvestTimeEntryImporter, self).cache_query()
|
||||
return query.filter(self.model_class.spent_date >= self.start_date)\
|
||||
.filter(self.model_class.spent_date <= self.end_date)
|
||||
query = super().cache_query()
|
||||
|
||||
if self.start_date:
|
||||
query = query.filter(self.model_class.spent_date >= self.start_date)
|
||||
if self.end_date:
|
||||
query = query.filter(self.model_class.spent_date <= self.end_date)
|
||||
|
||||
return query
|
||||
|
||||
def get_harvest_project(self, project_id):
|
||||
if hasattr(self, 'harvest_projects_by_id'):
|
||||
return self.harvest_projects_by_id.get(project_id)
|
||||
|
||||
model = self.model
|
||||
return self.session.query(model.HarvestCacheProject)\
|
||||
.filter(model.HarvestCacheProject.id == project_id)\
|
||||
.first()
|
||||
|
||||
def update_object(self, entry, data, local_data=None):
|
||||
entry = super().update_object(entry, data, local_data)
|
||||
model = self.model
|
||||
|
||||
if 'project_id' in self.fields:
|
||||
project_id = data['project_id']
|
||||
project = self.get_harvest_project(project_id)
|
||||
if not project:
|
||||
logger = log.warning if self.warn_for_unknown_project else log.debug
|
||||
logger("unknown project id %s for time entry id %s: %s",
|
||||
project_id, entry.id, entry)
|
||||
if self.auto_create_unknown_project:
|
||||
project = model.HarvestCacheProject()
|
||||
project.id = project_id
|
||||
project.name = "(unknown)"
|
||||
self.session.add(project)
|
||||
if hasattr(self, 'harvest_projects_by_id'):
|
||||
self.harvest_projects_by_id[project_id] = project
|
||||
elif entry.project_id:
|
||||
entry.project_id = None
|
||||
|
||||
return entry
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2022 Lance Edgar
|
||||
# Copyright © 2010-2023 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
|
@ -34,11 +34,11 @@ class FromRattailToRattailHarvestMixin(object):
|
|||
"""
|
||||
|
||||
def add_harvest_importers(self, importers):
|
||||
importers['HarvestUser'] = HarvestUserImporter
|
||||
importers['HarvestClient'] = HarvestClientImporter
|
||||
importers['HarvestProject'] = HarvestProjectImporter
|
||||
importers['HarvestTask'] = HarvestTaskImporter
|
||||
importers['HarvestTimeEntry'] = HarvestTimeEntryImporter
|
||||
importers['HarvestCacheUser'] = HarvestCacheUserImporter
|
||||
importers['HarvestCacheClient'] = HarvestCacheClientImporter
|
||||
importers['HarvestCacheProject'] = HarvestCacheProjectImporter
|
||||
importers['HarvestCacheTask'] = HarvestCacheTaskImporter
|
||||
importers['HarvestCacheTimeEntry'] = HarvestCacheTimeEntryImporter
|
||||
return importers
|
||||
|
||||
|
||||
|
@ -46,17 +46,26 @@ class FromRattailToRattailHarvestMixin(object):
|
|||
# harvest cache models
|
||||
##############################
|
||||
|
||||
class HarvestUserImporter(base.FromRattail, rattail_harvest_importing.model.HarvestUserImporter):
|
||||
class HarvestCacheUserImporter(base.FromRattail, rattail_harvest_importing.model.HarvestCacheUserImporter):
|
||||
pass
|
||||
|
||||
class HarvestClientImporter(base.FromRattail, rattail_harvest_importing.model.HarvestClientImporter):
|
||||
class HarvestCacheClientImporter(base.FromRattail, rattail_harvest_importing.model.HarvestCacheClientImporter):
|
||||
pass
|
||||
|
||||
class HarvestProjectImporter(base.FromRattail, rattail_harvest_importing.model.HarvestProjectImporter):
|
||||
class HarvestCacheProjectImporter(base.FromRattail, rattail_harvest_importing.model.HarvestCacheProjectImporter):
|
||||
pass
|
||||
|
||||
class HarvestTaskImporter(base.FromRattail, rattail_harvest_importing.model.HarvestTaskImporter):
|
||||
class HarvestCacheTaskImporter(base.FromRattail, rattail_harvest_importing.model.HarvestCacheTaskImporter):
|
||||
pass
|
||||
|
||||
class HarvestTimeEntryImporter(base.FromRattail, rattail_harvest_importing.model.HarvestTimeEntryImporter):
|
||||
pass
|
||||
class HarvestCacheTimeEntryImporter(base.FromRattail, rattail_harvest_importing.model.HarvestCacheTimeEntryImporter):
|
||||
|
||||
def query(self):
|
||||
query = super().query()
|
||||
|
||||
if self.start_date:
|
||||
query = query.filter(self.model_class.spent_date >= self.start_date)
|
||||
if self.end_date:
|
||||
query = query.filter(self.model_class.spent_date <= self.end_date)
|
||||
|
||||
return query
|
||||
|
|
73
rattail_harvest/importing/versions.py
Normal file
73
rattail_harvest/importing/versions.py
Normal file
|
@ -0,0 +1,73 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2023 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/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
Rattail -> Rattail "versions" data import
|
||||
"""
|
||||
|
||||
from rattail.importing import versions as base
|
||||
|
||||
|
||||
class HarvestVersionMixin(object):
|
||||
|
||||
def add_harvest_importers(self, importers):
|
||||
importers['HarvestCacheUser'] = HarvestCacheUserImporter
|
||||
importers['HarvestCacheClient'] = HarvestCacheClientImporter
|
||||
importers['HarvestCacheProject'] = HarvestCacheProjectImporter
|
||||
importers['HarvestCacheTask'] = HarvestCacheTaskImporter
|
||||
importers['HarvestCacheTimeEntry'] = HarvestCacheTimeEntryImporter
|
||||
return importers
|
||||
|
||||
|
||||
class HarvestCacheUserImporter(base.VersionImporter):
|
||||
|
||||
@property
|
||||
def host_model_class(self):
|
||||
return self.model.HarvestCacheUser
|
||||
|
||||
|
||||
class HarvestCacheClientImporter(base.VersionImporter):
|
||||
|
||||
@property
|
||||
def host_model_class(self):
|
||||
return self.model.HarvestCacheClient
|
||||
|
||||
|
||||
class HarvestCacheProjectImporter(base.VersionImporter):
|
||||
|
||||
@property
|
||||
def host_model_class(self):
|
||||
return self.model.HarvestCacheProject
|
||||
|
||||
|
||||
class HarvestCacheTaskImporter(base.VersionImporter):
|
||||
|
||||
@property
|
||||
def host_model_class(self):
|
||||
return self.model.HarvestCacheTask
|
||||
|
||||
|
||||
class HarvestCacheTimeEntryImporter(base.VersionImporter):
|
||||
|
||||
@property
|
||||
def host_model_class(self):
|
||||
return self.model.HarvestCacheTimeEntry
|
106
setup.py
106
setup.py
|
@ -1,106 +0,0 @@
|
|||
# -*- 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/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
rattail-harvest setup script
|
||||
"""
|
||||
|
||||
import os
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
exec(open(os.path.join(here, 'rattail_harvest', '_version.py')).read())
|
||||
README = open(os.path.join(here, 'README.rst')).read()
|
||||
|
||||
|
||||
requires = [
|
||||
#
|
||||
# Version numbers within comments below have specific meanings.
|
||||
# Basically the 'low' value is a "soft low," and 'high' a "soft high."
|
||||
# In other words:
|
||||
#
|
||||
# If either a 'low' or 'high' value exists, the primary point to be
|
||||
# made about the value is that it represents the most current (stable)
|
||||
# version available for the package (assuming typical public access
|
||||
# methods) whenever this project was started and/or documented.
|
||||
# Therefore:
|
||||
#
|
||||
# If a 'low' version is present, you should know that attempts to use
|
||||
# versions of the package significantly older than the 'low' version
|
||||
# may not yield happy results. (A "hard" high limit may or may not be
|
||||
# indicated by a true version requirement.)
|
||||
#
|
||||
# Similarly, if a 'high' version is present, and especially if this
|
||||
# project has laid dormant for a while, you may need to refactor a bit
|
||||
# when attempting to support a more recent version of the package. (A
|
||||
# "hard" low limit should be indicated by a true version requirement
|
||||
# when a 'high' version is present.)
|
||||
#
|
||||
# In any case, developers and other users are encouraged to play
|
||||
# outside the lines with regard to these soft limits. If bugs are
|
||||
# encountered then they should be filed as such.
|
||||
#
|
||||
# package # low high
|
||||
|
||||
'invoke', # 1.5.0
|
||||
'rattail[db]', # 0.9.246
|
||||
]
|
||||
|
||||
|
||||
setup(
|
||||
name = "rattail-harvest",
|
||||
version = __version__,
|
||||
author = "Lance Edgar",
|
||||
author_email = "lance@edbob.org",
|
||||
url = "https://rattailproject.org/",
|
||||
description = "Rattail integration package for Harvest",
|
||||
long_description = README,
|
||||
|
||||
classifiers = [
|
||||
'Development Status :: 3 - Alpha',
|
||||
'Environment :: Console',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
|
||||
'Natural Language :: English',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Topic :: Office/Business',
|
||||
'Topic :: Software Development :: Libraries :: Python Modules',
|
||||
],
|
||||
|
||||
install_requires = requires,
|
||||
packages = find_packages(),
|
||||
include_package_data = True,
|
||||
|
||||
entry_points = {
|
||||
|
||||
'rattail.commands': [
|
||||
'import-harvest = rattail_harvest.commands:ImportHarvest',
|
||||
],
|
||||
|
||||
'rattail.importing': [
|
||||
'to_rattail.from_harvest.import = rattail_harvest.importing.harvest:FromHarvestToRattail',
|
||||
],
|
||||
},
|
||||
)
|
26
tasks.py
26
tasks.py
|
@ -2,7 +2,7 @@
|
|||
################################################################################
|
||||
#
|
||||
# Rattail -- Retail Software Framework
|
||||
# Copyright © 2010-2022 Lance Edgar
|
||||
# Copyright © 2010-2024 Lance Edgar
|
||||
#
|
||||
# This file is part of Rattail.
|
||||
#
|
||||
|
@ -25,24 +25,36 @@ Tasks for rattail-harvest
|
|||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
|
||||
from invoke import task
|
||||
|
||||
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
exec(open(os.path.join(here, 'rattail_harvest', '_version.py')).read())
|
||||
__version__ = None
|
||||
pattern = re.compile(r'^version = "(\d+\.\d+\.\d+)"$')
|
||||
with open(os.path.join(here, 'pyproject.toml'), 'rt') as f:
|
||||
for line in f:
|
||||
line = line.rstrip('\n')
|
||||
match = pattern.match(line)
|
||||
if match:
|
||||
__version__ = match.group(1)
|
||||
break
|
||||
if not __version__:
|
||||
raise RuntimeError("could not parse version!")
|
||||
|
||||
|
||||
@task
|
||||
def release(ctx):
|
||||
def release(c):
|
||||
"""
|
||||
Release a new version of rattail-harvest
|
||||
"""
|
||||
# rebuild local tar.gz file for distribution
|
||||
shutil.rmtree('rattail_harvest.egg-info')
|
||||
ctx.run('python setup.py sdist --formats=gztar')
|
||||
if os.path.exists('rattail_harvest.egg-info'):
|
||||
shutil.rmtree('rattail_harvest.egg-info')
|
||||
c.run('python -m build --sdist')
|
||||
|
||||
# upload to public PyPI
|
||||
filename = 'rattail-harvest-{}.tar.gz'.format(__version__)
|
||||
ctx.run('twine upload dist/{}'.format(filename))
|
||||
filename = f'rattail_harvest-{__version__}.tar.gz'
|
||||
c.run(f'twine upload dist/{filename}')
|
||||
|
|
Loading…
Reference in a new issue