Compare commits
22 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
212e6ffc92 | ||
![]() |
65b9d059e8 | ||
![]() |
5884901d75 | ||
![]() |
52a1d15f7a | ||
![]() |
945d595329 | ||
![]() |
29c3429d2e | ||
![]() |
6eb483fe90 | ||
![]() |
3ea20c2d8c | ||
![]() |
eb904aea3b | ||
![]() |
f388f2d6cf | ||
![]() |
a1c9199416 | ||
![]() |
fc70b3c8ae | ||
![]() |
4c64dbc536 | ||
![]() |
15ea726b9b | ||
![]() |
8da3f89524 | ||
![]() |
f3e05124c3 | ||
![]() |
cc48cf5013 | ||
![]() |
d35baf15f6 | ||
![]() |
4bad4e262c | ||
![]() |
a270d1dcc2 | ||
![]() |
b8fd9c3022 | ||
![]() |
14e021db0d |
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1 +1,4 @@
|
|||
*~
|
||||
*.pyc
|
||||
dist/
|
||||
tailbone_harvest.egg-info/
|
||||
|
|
17
CHANGELOG.md
17
CHANGELOG.md
|
@ -5,6 +5,23 @@ All notable changes to tailbone-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.1 (2024-07-01)
|
||||
|
||||
### Fix
|
||||
|
||||
- remove incorrect entry points
|
||||
|
||||
## v0.3.0 (2024-06-10)
|
||||
|
||||
### Feat
|
||||
|
||||
- switch from setup.cfg to pyproject.toml + hatchling
|
||||
|
||||
## [0.2.0] - 2024-06-06
|
||||
Catch-up release.
|
||||
### Changed
|
||||
- lots of things...
|
||||
|
||||
## [0.1.0] - 2022-01-29
|
||||
### Added
|
||||
- Initial version.
|
||||
|
|
11
README.md
Normal file
11
README.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
|
||||
# tailbone-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 @@
|
|||
|
||||
tailbone-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/
|
42
pyproject.toml
Normal file
42
pyproject.toml
Normal file
|
@ -0,0 +1,42 @@
|
|||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
|
||||
[project]
|
||||
name = "tailbone-harvest"
|
||||
version = "0.3.1"
|
||||
description = "Tailbone 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 :: Web Environment",
|
||||
"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-harvest",
|
||||
"Tailbone",
|
||||
]
|
||||
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://rattailproject.org"
|
||||
Repository = "https://forgejo.wuttaproject.org/rattail/tailbone-harvest"
|
||||
Changelog = "https://forgejo.wuttaproject.org/rattail/tailbone-harvest/src/branch/master/CHANGELOG.md"
|
||||
|
||||
|
||||
[tool.commitizen]
|
||||
version_provider = "pep621"
|
||||
tag_format = "v$version"
|
||||
update_changelog_on_bump = true
|
96
setup.py
96
setup.py
|
@ -1,96 +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/>.
|
||||
#
|
||||
################################################################################
|
||||
"""
|
||||
tailbone-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, 'tailbone_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-harvest', # 0.1.0
|
||||
'Tailbone', # 0.8.199
|
||||
]
|
||||
|
||||
|
||||
setup(
|
||||
name = "tailbone-harvest",
|
||||
version = __version__,
|
||||
author = "Lance Edgar",
|
||||
author_email = "lance@edbob.org",
|
||||
url = "https://rattailproject.org/",
|
||||
description = "Tailbone integration package for Harvest",
|
||||
long_description = README,
|
||||
|
||||
classifiers = [
|
||||
'Development Status :: 3 - Alpha',
|
||||
'Environment :: Web Environment',
|
||||
'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,
|
||||
)
|
|
@ -1,3 +1,6 @@
|
|||
# -*- coding: utf-8; -*-
|
||||
|
||||
__version__ = '0.1.0'
|
||||
from importlib.metadata import version
|
||||
|
||||
|
||||
__version__ = version('tailbone-harvest')
|
||||
|
|
23
tailbone_harvest/templates/harvest-util.mako
Normal file
23
tailbone_harvest/templates/harvest-util.mako
Normal file
|
@ -0,0 +1,23 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
|
||||
<%def name="render_xref_buttons()">
|
||||
<b-button type="is-primary"
|
||||
% if harvest_url:
|
||||
tag="a" href="${harvest_url}" target="_blank"
|
||||
% else:
|
||||
disabled title="${harvest_why_no_url}"
|
||||
% endif
|
||||
icon-pack="fas"
|
||||
icon-left="external-link-alt">
|
||||
View in Harvest
|
||||
</b-button>
|
||||
</%def>
|
||||
|
||||
<%def name="render_xref_helper()">
|
||||
<nav class="panel">
|
||||
<p class="panel-heading">Cross-Reference</p>
|
||||
<div class="panel-block buttons">
|
||||
${self.render_xref_buttons()}
|
||||
</div>
|
||||
</nav>
|
||||
</%def>
|
45
tailbone_harvest/templates/harvest/time-entries/view.mako
Normal file
45
tailbone_harvest/templates/harvest/time-entries/view.mako
Normal file
|
@ -0,0 +1,45 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/master/view.mako" />
|
||||
|
||||
<%def name="object_helpers()">
|
||||
${parent.object_helpers()}
|
||||
${self.render_import_helper()}
|
||||
</%def>
|
||||
|
||||
<%def name="render_import_helper()">
|
||||
% if master.has_perm('import_from_harvest'):
|
||||
<nav class="panel">
|
||||
<p class="panel-heading">Re-Import</p>
|
||||
<div class="panel-block buttons">
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
% if master.has_perm('import_from_harvest'):
|
||||
${h.form(master.get_action_url('import_from_harvest', instance), **{'@submit': 'importFromHarvestSubmitting = true'})}
|
||||
${h.csrf_token(request)}
|
||||
<b-button type="is-primary"
|
||||
native-type="submit"
|
||||
icon-pack="fas"
|
||||
icon-left="redo"
|
||||
:disabled="importFromHarvestSubmitting">
|
||||
{{ importFromHarvestSubmitting ? "Working, please wait..." : "Re-Import from Harvest" }}
|
||||
</b-button>
|
||||
${h.end_form()}
|
||||
% endif
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="modify_this_page_vars()">
|
||||
${parent.modify_this_page_vars()}
|
||||
% if master.has_perm('import_from_harvest'):
|
||||
<script type="text/javascript">
|
||||
|
||||
ThisPageData.importFromHarvestSubmitting = false
|
||||
|
||||
</script>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
|
||||
${parent.body()}
|
16
tailbone_harvest/templates/harvest/users/view.mako
Normal file
16
tailbone_harvest/templates/harvest/users/view.mako
Normal file
|
@ -0,0 +1,16 @@
|
|||
## -*- coding: utf-8; -*-
|
||||
<%inherit file="/master/view.mako" />
|
||||
|
||||
<%def name="page_content()">
|
||||
|
||||
% if instance.avatar_url:
|
||||
<div style="margin: 1rem;">
|
||||
<img src="${instance.avatar_url}" />
|
||||
</div>
|
||||
% endif
|
||||
|
||||
${parent.page_content()}
|
||||
</%def>
|
||||
|
||||
|
||||
${parent.body()}
|
|
@ -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,18 +24,19 @@
|
|||
Harvest Client views
|
||||
"""
|
||||
|
||||
from rattail_harvest.db.model import HarvestClient
|
||||
from rattail_harvest.db.model import HarvestCacheClient
|
||||
from rattail_harvest.harvest.config import get_harvest_url
|
||||
|
||||
from webhelpers2.html import HTML, tags
|
||||
|
||||
from .master import HarvestMasterView
|
||||
|
||||
|
||||
class HarvestClientView(HarvestMasterView):
|
||||
class HarvestCacheClientView(HarvestMasterView):
|
||||
"""
|
||||
Master view for Harvest Clients
|
||||
"""
|
||||
model_class = HarvestClient
|
||||
model_class = HarvestCacheClient
|
||||
url_prefix = '/harvest/clients'
|
||||
route_prefix = 'harvest.clients'
|
||||
|
||||
|
@ -46,18 +47,25 @@ class HarvestClientView(HarvestMasterView):
|
|||
]
|
||||
|
||||
def configure_grid(self, g):
|
||||
super(HarvestClientView, self).configure_grid(g)
|
||||
super().configure_grid(g)
|
||||
|
||||
g.filters['name'].default_active = True
|
||||
g.filters['name'].default_verb = 'contains'
|
||||
|
||||
g.filters['is_active'].default_active = True
|
||||
g.filters['is_active'].default_verb = 'is_true'
|
||||
|
||||
g.set_sort_defaults('name')
|
||||
|
||||
g.set_link('id')
|
||||
g.set_link('name')
|
||||
|
||||
def grid_extra_class(self, client, i):
|
||||
if not client.is_active:
|
||||
return 'warning'
|
||||
|
||||
def configure_form(self, f):
|
||||
super(HarvestClientView, self).configure_form(f)
|
||||
super().configure_form(f)
|
||||
|
||||
# projects
|
||||
f.set_renderer('projects', self.render_projects)
|
||||
|
@ -77,6 +85,26 @@ class HarvestClientView(HarvestMasterView):
|
|||
items.append(HTML.tag('li', c=[tags.link_to(text, url)]))
|
||||
return HTML.tag('ul', c=items)
|
||||
|
||||
def get_xref_buttons(self, client):
|
||||
buttons = super().get_xref_buttons(client)
|
||||
model = self.model
|
||||
|
||||
# harvest proper
|
||||
url = get_harvest_url(self.rattail_config)
|
||||
if url:
|
||||
url = '{}/clients'.format(url)
|
||||
buttons.append(self.make_xref_button(url=url,
|
||||
text="View in Harvest"))
|
||||
|
||||
return buttons
|
||||
|
||||
|
||||
def defaults(config, **kwargs):
|
||||
base = globals()
|
||||
|
||||
HarvestCacheClientView = kwargs.get('HarvestCacheClientView', base['HarvestCacheClientView'])
|
||||
HarvestCacheClientView.defaults(config)
|
||||
|
||||
|
||||
def includeme(config):
|
||||
HarvestClientView.defaults(config)
|
||||
defaults(config)
|
||||
|
|
|
@ -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,10 @@
|
|||
Harvest master view
|
||||
"""
|
||||
|
||||
from rattail_harvest.db.model import HarvestCacheTimeEntry
|
||||
|
||||
from webhelpers2.html import tags
|
||||
|
||||
from tailbone.views import MasterView
|
||||
|
||||
|
||||
|
@ -32,10 +36,58 @@ class HarvestMasterView(MasterView):
|
|||
Base class for Harvest master views
|
||||
"""
|
||||
creatable = False
|
||||
editable = False
|
||||
deletable = False
|
||||
touchable = True
|
||||
has_versions = True
|
||||
model_row_class = HarvestCacheTimeEntry
|
||||
|
||||
labels = {
|
||||
'id': "ID",
|
||||
'user_id': "User ID",
|
||||
'client_id': "Client ID",
|
||||
'project_id': "Project ID",
|
||||
'task_id': "Task ID",
|
||||
'invoice_id': "Invoice ID",
|
||||
}
|
||||
|
||||
row_labels = {
|
||||
'id': "ID",
|
||||
}
|
||||
|
||||
def configure_form(self, f):
|
||||
super(HarvestMasterView, self).configure_form(f)
|
||||
f.remove('time_entries')
|
||||
|
||||
def render_harvest_user(self, obj, field):
|
||||
user = getattr(obj, field)
|
||||
if user:
|
||||
text = str(user)
|
||||
url = self.request.route_url('harvest.users.view', uuid=user.uuid)
|
||||
return tags.link_to(text, url)
|
||||
|
||||
def render_harvest_client(self, obj, field):
|
||||
client = getattr(obj, field)
|
||||
if client:
|
||||
text = str(client)
|
||||
url = self.request.route_url('harvest.clients.view', uuid=client.uuid)
|
||||
return tags.link_to(text, url)
|
||||
|
||||
def render_harvest_project(self, obj, field):
|
||||
project = getattr(obj, field)
|
||||
if project:
|
||||
text = str(project)
|
||||
url = self.request.route_url('harvest.projects.view', uuid=project.uuid)
|
||||
return tags.link_to(text, url)
|
||||
|
||||
def render_harvest_task(self, obj, field):
|
||||
task = getattr(obj, field)
|
||||
if task:
|
||||
text = str(task)
|
||||
url = self.request.route_url('harvest.tasks.view', uuid=task.uuid)
|
||||
return tags.link_to(text, url)
|
||||
|
||||
def configure_row_grid(self, g):
|
||||
super(HarvestMasterView, self).configure_row_grid(g)
|
||||
g.set_sort_defaults('spent_date', 'desc')
|
||||
|
||||
def row_view_action_url(self, entry, i):
|
||||
return self.request.route_url('harvest.time_entries.view', uuid=entry.uuid)
|
||||
|
|
|
@ -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,19 +24,22 @@
|
|||
Harvest Project views
|
||||
"""
|
||||
|
||||
from rattail_harvest.db.model import HarvestProject
|
||||
from rattail_harvest.db.model import HarvestCacheProject
|
||||
from rattail_harvest.harvest.config import get_harvest_url
|
||||
|
||||
from .master import HarvestMasterView
|
||||
|
||||
|
||||
class HarvestProjectView(HarvestMasterView):
|
||||
class HarvestCacheProjectView(HarvestMasterView):
|
||||
"""
|
||||
Master view for Harvest Projects
|
||||
"""
|
||||
model_class = HarvestProject
|
||||
model_class = HarvestCacheProject
|
||||
url_prefix = '/harvest/projects'
|
||||
route_prefix = 'harvest.projects'
|
||||
|
||||
has_rows = True
|
||||
|
||||
grid_columns = [
|
||||
'id',
|
||||
'client',
|
||||
|
@ -49,26 +52,89 @@ class HarvestProjectView(HarvestMasterView):
|
|||
'fee',
|
||||
]
|
||||
|
||||
row_grid_columns = [
|
||||
'id',
|
||||
'spent_date',
|
||||
'user',
|
||||
'client',
|
||||
'task',
|
||||
'hours',
|
||||
]
|
||||
|
||||
def configure_grid(self, g):
|
||||
super(HarvestProjectView, self).configure_grid(g)
|
||||
super().configure_grid(g)
|
||||
model = self.model
|
||||
|
||||
g.set_joiner('client', lambda q: q.outerjoin(model.HarvestClient))
|
||||
g.set_sorter('client', model.HarvestClient.name)
|
||||
g.set_filter('client', model.HarvestClient.name, label="Client Name")
|
||||
g.set_joiner('client', lambda q: q.outerjoin(model.HarvestCacheClient))
|
||||
g.set_sorter('client', model.HarvestCacheClient.name)
|
||||
g.set_filter('client', model.HarvestCacheClient.name, label="Client Name")
|
||||
g.filters['client'].default_active = True
|
||||
g.filters['client'].default_verb = 'contains'
|
||||
|
||||
g.filters['is_active'].default_active = True
|
||||
g.filters['is_active'].default_verb = 'is_true'
|
||||
|
||||
g.set_type('hourly_rate', 'currency')
|
||||
g.set_type('fee', 'currency')
|
||||
|
||||
g.set_sort_defaults('client')
|
||||
|
||||
g.set_filters_sequence([
|
||||
'id',
|
||||
'name',
|
||||
'client',
|
||||
])
|
||||
|
||||
g.set_link('id')
|
||||
g.set_link('client')
|
||||
g.set_link('name')
|
||||
g.set_link('code')
|
||||
|
||||
def grid_extra_class(self, project, i):
|
||||
if not project.is_active:
|
||||
return 'warning'
|
||||
|
||||
def configure_form(self, f):
|
||||
super().configure_form(f)
|
||||
|
||||
f.set_type('hourly_rate', 'currency')
|
||||
|
||||
if self.editing:
|
||||
f.remove('client')
|
||||
f.set_type('over_budget_notification_date', 'date_jquery')
|
||||
f.set_type('starts_on', 'date_jquery')
|
||||
f.set_type('ends_on', 'date_jquery')
|
||||
f.set_readonly('created_at')
|
||||
f.set_readonly('updated_at')
|
||||
|
||||
def get_xref_buttons(self, project):
|
||||
buttons = super().get_xref_buttons(project)
|
||||
model = self.model
|
||||
|
||||
# harvest
|
||||
url = get_harvest_url(self.rattail_config)
|
||||
if url:
|
||||
url = '{}/projects/{}'.format(url, project.id)
|
||||
buttons.append(self.make_xref_button(url=url,
|
||||
text="View in Harvest"))
|
||||
|
||||
return buttons
|
||||
|
||||
def get_row_data(self, project):
|
||||
model = self.model
|
||||
return self.Session.query(model.HarvestCacheTimeEntry)\
|
||||
.filter(model.HarvestCacheTimeEntry.project == project)
|
||||
|
||||
def get_parent(self, entry):
|
||||
return entry.project
|
||||
|
||||
|
||||
def defaults(config, **kwargs):
|
||||
base = globals()
|
||||
|
||||
HarvestCacheProjectView = kwargs.get('HarvestCacheProjectView', base['HarvestCacheProjectView'])
|
||||
HarvestCacheProjectView.defaults(config)
|
||||
|
||||
|
||||
def includeme(config):
|
||||
HarvestProjectView.defaults(config)
|
||||
defaults(config)
|
||||
|
|
|
@ -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,16 +24,17 @@
|
|||
Harvest Task views
|
||||
"""
|
||||
|
||||
from rattail_harvest.db.model import HarvestTask
|
||||
from rattail_harvest.db.model import HarvestCacheTask
|
||||
from rattail_harvest.harvest.config import get_harvest_url
|
||||
|
||||
from .master import HarvestMasterView
|
||||
|
||||
|
||||
class HarvestTaskView(HarvestMasterView):
|
||||
class HarvestCacheTaskView(HarvestMasterView):
|
||||
"""
|
||||
Master view for Harvest Tasks
|
||||
"""
|
||||
model_class = HarvestTask
|
||||
model_class = HarvestCacheTask
|
||||
url_prefix = '/harvest/tasks'
|
||||
route_prefix = 'harvest.tasks'
|
||||
|
||||
|
@ -47,7 +48,7 @@ class HarvestTaskView(HarvestMasterView):
|
|||
]
|
||||
|
||||
def configure_grid(self, g):
|
||||
super(HarvestTaskView, self).configure_grid(g)
|
||||
super().configure_grid(g)
|
||||
|
||||
g.set_sort_defaults('name')
|
||||
|
||||
|
@ -55,11 +56,31 @@ class HarvestTaskView(HarvestMasterView):
|
|||
g.set_link('name')
|
||||
|
||||
def configure_form(self, f):
|
||||
super(HarvestTaskView, self).configure_form(f)
|
||||
super().configure_form(f)
|
||||
|
||||
# time_entries
|
||||
f.remove_field('time_entries')
|
||||
|
||||
def get_xref_buttons(self, task):
|
||||
buttons = super().get_xref_buttons(task)
|
||||
model = self.model
|
||||
|
||||
# harvest
|
||||
url = get_harvest_url(self.rattail_config)
|
||||
if url:
|
||||
url = '{}/tasks'.format(url)
|
||||
buttons.append(self.make_xref_button(url=url,
|
||||
text="View in Harvest"))
|
||||
|
||||
return buttons
|
||||
|
||||
|
||||
def defaults(config, **kwargs):
|
||||
base = globals()
|
||||
|
||||
HarvestCacheTaskView = kwargs.get('HarvestCacheTaskView', base['HarvestCacheTaskView'])
|
||||
HarvestCacheTaskView.defaults(config)
|
||||
|
||||
|
||||
def includeme(config):
|
||||
HarvestTaskView.defaults(config)
|
||||
defaults(config)
|
||||
|
|
|
@ -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,27 +24,20 @@
|
|||
Harvest Time Entry views
|
||||
"""
|
||||
|
||||
from rattail_harvest.db.model import HarvestTimeEntry
|
||||
from rattail_harvest.db.model import HarvestCacheTimeEntry
|
||||
from rattail_harvest.harvest.config import get_harvest_url
|
||||
|
||||
from .master import HarvestMasterView
|
||||
|
||||
|
||||
class HarvestTimeEntryView(HarvestMasterView):
|
||||
class HarvestCacheTimeEntryView(HarvestMasterView):
|
||||
"""
|
||||
Master view for Harvest Time Entries
|
||||
"""
|
||||
model_class = HarvestTimeEntry
|
||||
model_class = HarvestCacheTimeEntry
|
||||
url_prefix = '/harvest/time-entries'
|
||||
route_prefix = 'harvest.time_entries'
|
||||
|
||||
labels = {
|
||||
'user_id': "User ID",
|
||||
'client_id': "Client ID",
|
||||
'project_id': "Project ID",
|
||||
'task_id': "Task ID",
|
||||
'invoice_id': "Invoice ID",
|
||||
}
|
||||
|
||||
grid_columns = [
|
||||
'id',
|
||||
'spent_date',
|
||||
|
@ -57,7 +50,7 @@ class HarvestTimeEntryView(HarvestMasterView):
|
|||
]
|
||||
|
||||
def configure_grid(self, g):
|
||||
super(HarvestTimeEntryView, self).configure_grid(g)
|
||||
super().configure_grid(g)
|
||||
|
||||
g.set_type('hours', 'duration_hours')
|
||||
|
||||
|
@ -68,6 +61,103 @@ class HarvestTimeEntryView(HarvestMasterView):
|
|||
g.set_link('client')
|
||||
g.set_link('notes')
|
||||
|
||||
def configure_form(self, f):
|
||||
super().configure_form(f)
|
||||
|
||||
# make sure id is first field
|
||||
f.remove('id')
|
||||
f.insert(0, 'id')
|
||||
|
||||
# user
|
||||
f.remove('user_id')
|
||||
f.set_renderer('user', self.render_harvest_user)
|
||||
|
||||
# client
|
||||
f.remove('client_id')
|
||||
f.set_renderer('client', self.render_harvest_client)
|
||||
|
||||
# project
|
||||
f.remove('project_id')
|
||||
f.set_renderer('project', self.render_harvest_project)
|
||||
|
||||
# task
|
||||
f.remove('task_id')
|
||||
f.set_renderer('task', self.render_harvest_task)
|
||||
|
||||
# hours
|
||||
f.set_renderer('hours', self.render_hours)
|
||||
|
||||
f.set_type('notes', 'text')
|
||||
|
||||
f.set_type('billable_rate', 'currency')
|
||||
f.set_type('cost_rate', 'currency')
|
||||
|
||||
def render_hours(self, entry, field):
|
||||
hours = getattr(entry, field)
|
||||
app = self.get_rattail_app()
|
||||
duration = app.render_duration(hours=hours)
|
||||
return f"{hours} ({duration})"
|
||||
|
||||
def get_xref_buttons(self, entry):
|
||||
buttons = super().get_xref_buttons(entry)
|
||||
model = self.model
|
||||
|
||||
# harvest
|
||||
url = get_harvest_url(self.rattail_config)
|
||||
if url:
|
||||
url = '{}/time/day/{}/{}'.format(
|
||||
url,
|
||||
entry.spent_date.strftime('%Y/%m/%d'),
|
||||
entry.user_id)
|
||||
buttons.append(self.make_xref_button(url=url,
|
||||
text="View in Harvest"))
|
||||
|
||||
return buttons
|
||||
|
||||
def import_from_harvest(self):
|
||||
app = self.get_rattail_app()
|
||||
handler = app.get_import_handler('to_rattail.from_harvest.import', require=True)
|
||||
importer = handler.get_importer('HarvestCacheTimeEntry')
|
||||
importer.session = self.Session()
|
||||
importer.setup()
|
||||
|
||||
cache_entry = self.get_instance()
|
||||
if self.oneoff_import(importer, local_object=cache_entry):
|
||||
self.request.session.flash(f"{self.get_model_title()} has been "
|
||||
f"(re-)imported from Harvest: {cache_entry}")
|
||||
else:
|
||||
self.request.session.flash("Import failed!", 'error')
|
||||
|
||||
return self.redirect(self.get_action_url('view', cache_entry))
|
||||
|
||||
@classmethod
|
||||
def defaults(cls, config):
|
||||
route_prefix = cls.get_route_prefix()
|
||||
instance_url_prefix = cls.get_instance_url_prefix()
|
||||
permission_prefix = cls.get_permission_prefix()
|
||||
model_title = cls.get_model_title()
|
||||
|
||||
# normal defaults
|
||||
cls._defaults(config)
|
||||
|
||||
# import from harvest
|
||||
config.add_tailbone_permission(permission_prefix,
|
||||
f'{permission_prefix}.import_from_harvest',
|
||||
f"Re-Import {model_title} from Harvest")
|
||||
config.add_route(f'{route_prefix}.import_from_harvest',
|
||||
f'{instance_url_prefix}/import-from-harvest',
|
||||
request_method='POST')
|
||||
config.add_view(cls, attr='import_from_harvest',
|
||||
route_name=f'{route_prefix}.import_from_harvest',
|
||||
permission=f'{permission_prefix}.import_from_harvest')
|
||||
|
||||
|
||||
def defaults(config, **kwargs):
|
||||
base = globals()
|
||||
|
||||
HarvestCacheTimeEntryView = kwargs.get('HarvestCacheTimeEntryView', base['HarvestCacheTimeEntryView'])
|
||||
HarvestCacheTimeEntryView.defaults(config)
|
||||
|
||||
|
||||
def includeme(config):
|
||||
HarvestTimeEntryView.defaults(config)
|
||||
defaults(config)
|
||||
|
|
|
@ -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,19 +24,27 @@
|
|||
Harvest User views
|
||||
"""
|
||||
|
||||
from rattail_harvest.db.model import HarvestUser
|
||||
from rattail_harvest.db.model import HarvestCacheUser
|
||||
from rattail_harvest.harvest.config import get_harvest_url
|
||||
|
||||
import colander
|
||||
|
||||
from tailbone import forms
|
||||
from .master import HarvestMasterView
|
||||
|
||||
|
||||
class HarvestUserView(HarvestMasterView):
|
||||
class HarvestCacheUserView(HarvestMasterView):
|
||||
"""
|
||||
Master view for Harvest Users
|
||||
"""
|
||||
model_class = HarvestUser
|
||||
model_class = HarvestCacheUser
|
||||
url_prefix = '/harvest/users'
|
||||
route_prefix = 'harvest.users'
|
||||
|
||||
labels = {
|
||||
'avatar_url': "Avatar URL",
|
||||
}
|
||||
|
||||
grid_columns = [
|
||||
'id',
|
||||
'first_name',
|
||||
|
@ -48,7 +56,11 @@ class HarvestUserView(HarvestMasterView):
|
|||
]
|
||||
|
||||
def configure_grid(self, g):
|
||||
super(HarvestUserView, self).configure_grid(g)
|
||||
super().configure_grid(g)
|
||||
model = self.model
|
||||
|
||||
g.set_joiner('person_name', lambda q: q.outerjoin(model.Person))
|
||||
g.set_filter('person_name', model.Person.display_name)
|
||||
|
||||
g.set_sort_defaults('first_name')
|
||||
|
||||
|
@ -58,11 +70,75 @@ class HarvestUserView(HarvestMasterView):
|
|||
g.set_link('email')
|
||||
|
||||
def configure_form(self, f):
|
||||
super(HarvestUserView, self).configure_form(f)
|
||||
super().configure_form(f)
|
||||
model = self.model
|
||||
user = f.model_instance
|
||||
|
||||
# person
|
||||
f.set_renderer('person', self.render_person)
|
||||
if self.creating or self.editing:
|
||||
if 'person' in f.fields:
|
||||
f.remove('person_uuid')
|
||||
f.replace('person', 'person_uuid')
|
||||
person_display = ""
|
||||
if self.request.method == 'POST':
|
||||
if self.request.POST.get('person_uuid'):
|
||||
person = self.Session.get(model.Person,
|
||||
self.request.POST['person_uuid'])
|
||||
if person:
|
||||
person_display = str(person)
|
||||
elif self.editing:
|
||||
person_display = str(user.person or '')
|
||||
people_url = self.request.route_url('people.autocomplete')
|
||||
f.set_widget('person_uuid', forms.widgets.JQueryAutocompleteWidget(
|
||||
field_display=person_display, service_url=people_url))
|
||||
f.set_validator('person_uuid', self.valid_person)
|
||||
f.set_label('person_uuid', "Person")
|
||||
else:
|
||||
f.remove('person_uuid')
|
||||
|
||||
f.set_type('weekly_capacity', 'duration')
|
||||
|
||||
f.set_type('default_hourly_rate', 'currency')
|
||||
f.set_type('cost_rate', 'currency')
|
||||
|
||||
f.set_renderer('avatar_url', self.render_url)
|
||||
|
||||
# timestamps
|
||||
if self.creating or self.editing:
|
||||
f.remove('created_at')
|
||||
f.remove('updated_at')
|
||||
|
||||
# time_entries
|
||||
# TODO: should add this as child rows/grid instead
|
||||
f.remove('time_entries')
|
||||
|
||||
def valid_person(self, node, value):
|
||||
model = self.model
|
||||
if value:
|
||||
person = self.Session.get(model.Person, value)
|
||||
if not person:
|
||||
raise colander.Invalid(node, "Person not found (you must *select* a record)")
|
||||
|
||||
def get_xref_buttons(self, user):
|
||||
buttons = super().get_xref_buttons(user)
|
||||
model = self.model
|
||||
|
||||
# harvest proper
|
||||
url = get_harvest_url(self.rattail_config)
|
||||
if url:
|
||||
url = '{}/team'.format(url)
|
||||
buttons.append(self.make_xref_button(url=url, text="View in Harvest"))
|
||||
|
||||
return buttons
|
||||
|
||||
|
||||
def defaults(config, **kwargs):
|
||||
base = globals()
|
||||
|
||||
HarvestCacheUserView = kwargs.get('HarvestCacheUserView', base['HarvestCacheUserView'])
|
||||
HarvestCacheUserView.defaults(config)
|
||||
|
||||
|
||||
def includeme(config):
|
||||
HarvestUserView.defaults(config)
|
||||
defaults(config)
|
||||
|
|
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 tailbone-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, 'tailbone_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 tailbone-harvest
|
||||
"""
|
||||
# rebuild local tar.gz file for distribution
|
||||
shutil.rmtree('tailbone_harvest.egg-info')
|
||||
ctx.run('python setup.py sdist --formats=gztar')
|
||||
if os.path.exists('tailbone_harvest.egg-info'):
|
||||
shutil.rmtree('tailbone_harvest.egg-info')
|
||||
c.run('python -m build --sdist')
|
||||
|
||||
# upload to public PyPI
|
||||
filename = 'tailbone-harvest-{}.tar.gz'.format(__version__)
|
||||
ctx.run('twine upload dist/{}'.format(filename))
|
||||
filename = f'tailbone_harvest-{__version__}.tar.gz'
|
||||
c.run(f'twine upload dist/{filename}')
|
||||
|
|
Loading…
Reference in a new issue