Compare commits
No commits in common. "master" and "v0.1.0" have entirely different histories.
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,4 +1 @@
|
||||||
*~
|
|
||||||
*.pyc
|
|
||||||
dist/
|
|
||||||
tailbone_harvest.egg-info/
|
tailbone_harvest.egg-info/
|
||||||
|
|
17
CHANGELOG.md
17
CHANGELOG.md
|
@ -5,23 +5,6 @@ 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/)
|
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).
|
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
|
## [0.1.0] - 2022-01-29
|
||||||
### Added
|
### Added
|
||||||
- Initial version.
|
- Initial version.
|
||||||
|
|
11
README.md
11
README.md
|
@ -1,11 +0,0 @@
|
||||||
|
|
||||||
# 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
Normal file
14
README.rst
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
|
||||||
|
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/
|
|
@ -1,42 +0,0 @@
|
||||||
|
|
||||||
[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
Normal file
96
setup.py
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
# -*- 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,6 +1,3 @@
|
||||||
# -*- coding: utf-8; -*-
|
# -*- coding: utf-8; -*-
|
||||||
|
|
||||||
from importlib.metadata import version
|
__version__ = '0.1.0'
|
||||||
|
|
||||||
|
|
||||||
__version__ = version('tailbone-harvest')
|
|
||||||
|
|
0
tailbone_harvest/templates/.keepme
Normal file
0
tailbone_harvest/templates/.keepme
Normal file
|
@ -1,23 +0,0 @@
|
||||||
## -*- 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>
|
|
|
@ -1,45 +0,0 @@
|
||||||
## -*- 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()}
|
|
|
@ -1,16 +0,0 @@
|
||||||
## -*- 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
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2023 Lance Edgar
|
# Copyright © 2010-2022 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -24,19 +24,18 @@
|
||||||
Harvest Client views
|
Harvest Client views
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from rattail_harvest.db.model import HarvestCacheClient
|
from rattail_harvest.db.model import HarvestClient
|
||||||
from rattail_harvest.harvest.config import get_harvest_url
|
|
||||||
|
|
||||||
from webhelpers2.html import HTML, tags
|
from webhelpers2.html import HTML, tags
|
||||||
|
|
||||||
from .master import HarvestMasterView
|
from .master import HarvestMasterView
|
||||||
|
|
||||||
|
|
||||||
class HarvestCacheClientView(HarvestMasterView):
|
class HarvestClientView(HarvestMasterView):
|
||||||
"""
|
"""
|
||||||
Master view for Harvest Clients
|
Master view for Harvest Clients
|
||||||
"""
|
"""
|
||||||
model_class = HarvestCacheClient
|
model_class = HarvestClient
|
||||||
url_prefix = '/harvest/clients'
|
url_prefix = '/harvest/clients'
|
||||||
route_prefix = 'harvest.clients'
|
route_prefix = 'harvest.clients'
|
||||||
|
|
||||||
|
@ -47,25 +46,18 @@ class HarvestCacheClientView(HarvestMasterView):
|
||||||
]
|
]
|
||||||
|
|
||||||
def configure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
super().configure_grid(g)
|
super(HarvestClientView, self).configure_grid(g)
|
||||||
|
|
||||||
g.filters['name'].default_active = True
|
g.filters['name'].default_active = True
|
||||||
g.filters['name'].default_verb = 'contains'
|
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_sort_defaults('name')
|
||||||
|
|
||||||
g.set_link('id')
|
g.set_link('id')
|
||||||
g.set_link('name')
|
g.set_link('name')
|
||||||
|
|
||||||
def grid_extra_class(self, client, i):
|
|
||||||
if not client.is_active:
|
|
||||||
return 'warning'
|
|
||||||
|
|
||||||
def configure_form(self, f):
|
def configure_form(self, f):
|
||||||
super().configure_form(f)
|
super(HarvestClientView, self).configure_form(f)
|
||||||
|
|
||||||
# projects
|
# projects
|
||||||
f.set_renderer('projects', self.render_projects)
|
f.set_renderer('projects', self.render_projects)
|
||||||
|
@ -85,26 +77,6 @@ class HarvestCacheClientView(HarvestMasterView):
|
||||||
items.append(HTML.tag('li', c=[tags.link_to(text, url)]))
|
items.append(HTML.tag('li', c=[tags.link_to(text, url)]))
|
||||||
return HTML.tag('ul', c=items)
|
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):
|
def includeme(config):
|
||||||
defaults(config)
|
HarvestClientView.defaults(config)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2023 Lance Edgar
|
# Copyright © 2010-2022 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -24,10 +24,6 @@
|
||||||
Harvest master view
|
Harvest master view
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from rattail_harvest.db.model import HarvestCacheTimeEntry
|
|
||||||
|
|
||||||
from webhelpers2.html import tags
|
|
||||||
|
|
||||||
from tailbone.views import MasterView
|
from tailbone.views import MasterView
|
||||||
|
|
||||||
|
|
||||||
|
@ -36,58 +32,10 @@ class HarvestMasterView(MasterView):
|
||||||
Base class for Harvest master views
|
Base class for Harvest master views
|
||||||
"""
|
"""
|
||||||
creatable = False
|
creatable = False
|
||||||
touchable = True
|
editable = False
|
||||||
|
deletable = False
|
||||||
has_versions = True
|
has_versions = True
|
||||||
model_row_class = HarvestCacheTimeEntry
|
|
||||||
|
|
||||||
labels = {
|
labels = {
|
||||||
'id': "ID",
|
'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
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2023 Lance Edgar
|
# Copyright © 2010-2022 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -24,22 +24,19 @@
|
||||||
Harvest Project views
|
Harvest Project views
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from rattail_harvest.db.model import HarvestCacheProject
|
from rattail_harvest.db.model import HarvestProject
|
||||||
from rattail_harvest.harvest.config import get_harvest_url
|
|
||||||
|
|
||||||
from .master import HarvestMasterView
|
from .master import HarvestMasterView
|
||||||
|
|
||||||
|
|
||||||
class HarvestCacheProjectView(HarvestMasterView):
|
class HarvestProjectView(HarvestMasterView):
|
||||||
"""
|
"""
|
||||||
Master view for Harvest Projects
|
Master view for Harvest Projects
|
||||||
"""
|
"""
|
||||||
model_class = HarvestCacheProject
|
model_class = HarvestProject
|
||||||
url_prefix = '/harvest/projects'
|
url_prefix = '/harvest/projects'
|
||||||
route_prefix = 'harvest.projects'
|
route_prefix = 'harvest.projects'
|
||||||
|
|
||||||
has_rows = True
|
|
||||||
|
|
||||||
grid_columns = [
|
grid_columns = [
|
||||||
'id',
|
'id',
|
||||||
'client',
|
'client',
|
||||||
|
@ -52,89 +49,26 @@ class HarvestCacheProjectView(HarvestMasterView):
|
||||||
'fee',
|
'fee',
|
||||||
]
|
]
|
||||||
|
|
||||||
row_grid_columns = [
|
|
||||||
'id',
|
|
||||||
'spent_date',
|
|
||||||
'user',
|
|
||||||
'client',
|
|
||||||
'task',
|
|
||||||
'hours',
|
|
||||||
]
|
|
||||||
|
|
||||||
def configure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
super().configure_grid(g)
|
super(HarvestProjectView, self).configure_grid(g)
|
||||||
model = self.model
|
model = self.model
|
||||||
|
|
||||||
g.set_joiner('client', lambda q: q.outerjoin(model.HarvestCacheClient))
|
g.set_joiner('client', lambda q: q.outerjoin(model.HarvestClient))
|
||||||
g.set_sorter('client', model.HarvestCacheClient.name)
|
g.set_sorter('client', model.HarvestClient.name)
|
||||||
g.set_filter('client', model.HarvestCacheClient.name, label="Client Name")
|
g.set_filter('client', model.HarvestClient.name, label="Client Name")
|
||||||
g.filters['client'].default_active = True
|
g.filters['client'].default_active = True
|
||||||
g.filters['client'].default_verb = 'contains'
|
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('hourly_rate', 'currency')
|
||||||
g.set_type('fee', 'currency')
|
g.set_type('fee', 'currency')
|
||||||
|
|
||||||
g.set_sort_defaults('client')
|
g.set_sort_defaults('client')
|
||||||
|
|
||||||
g.set_filters_sequence([
|
|
||||||
'id',
|
|
||||||
'name',
|
|
||||||
'client',
|
|
||||||
])
|
|
||||||
|
|
||||||
g.set_link('id')
|
g.set_link('id')
|
||||||
g.set_link('client')
|
g.set_link('client')
|
||||||
g.set_link('name')
|
g.set_link('name')
|
||||||
g.set_link('code')
|
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):
|
def includeme(config):
|
||||||
defaults(config)
|
HarvestProjectView.defaults(config)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2023 Lance Edgar
|
# Copyright © 2010-2022 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -24,17 +24,16 @@
|
||||||
Harvest Task views
|
Harvest Task views
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from rattail_harvest.db.model import HarvestCacheTask
|
from rattail_harvest.db.model import HarvestTask
|
||||||
from rattail_harvest.harvest.config import get_harvest_url
|
|
||||||
|
|
||||||
from .master import HarvestMasterView
|
from .master import HarvestMasterView
|
||||||
|
|
||||||
|
|
||||||
class HarvestCacheTaskView(HarvestMasterView):
|
class HarvestTaskView(HarvestMasterView):
|
||||||
"""
|
"""
|
||||||
Master view for Harvest Tasks
|
Master view for Harvest Tasks
|
||||||
"""
|
"""
|
||||||
model_class = HarvestCacheTask
|
model_class = HarvestTask
|
||||||
url_prefix = '/harvest/tasks'
|
url_prefix = '/harvest/tasks'
|
||||||
route_prefix = 'harvest.tasks'
|
route_prefix = 'harvest.tasks'
|
||||||
|
|
||||||
|
@ -48,7 +47,7 @@ class HarvestCacheTaskView(HarvestMasterView):
|
||||||
]
|
]
|
||||||
|
|
||||||
def configure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
super().configure_grid(g)
|
super(HarvestTaskView, self).configure_grid(g)
|
||||||
|
|
||||||
g.set_sort_defaults('name')
|
g.set_sort_defaults('name')
|
||||||
|
|
||||||
|
@ -56,31 +55,11 @@ class HarvestCacheTaskView(HarvestMasterView):
|
||||||
g.set_link('name')
|
g.set_link('name')
|
||||||
|
|
||||||
def configure_form(self, f):
|
def configure_form(self, f):
|
||||||
super().configure_form(f)
|
super(HarvestTaskView, self).configure_form(f)
|
||||||
|
|
||||||
# time_entries
|
# time_entries
|
||||||
f.remove_field('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):
|
def includeme(config):
|
||||||
defaults(config)
|
HarvestTaskView.defaults(config)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2023 Lance Edgar
|
# Copyright © 2010-2022 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -24,20 +24,27 @@
|
||||||
Harvest Time Entry views
|
Harvest Time Entry views
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from rattail_harvest.db.model import HarvestCacheTimeEntry
|
from rattail_harvest.db.model import HarvestTimeEntry
|
||||||
from rattail_harvest.harvest.config import get_harvest_url
|
|
||||||
|
|
||||||
from .master import HarvestMasterView
|
from .master import HarvestMasterView
|
||||||
|
|
||||||
|
|
||||||
class HarvestCacheTimeEntryView(HarvestMasterView):
|
class HarvestTimeEntryView(HarvestMasterView):
|
||||||
"""
|
"""
|
||||||
Master view for Harvest Time Entries
|
Master view for Harvest Time Entries
|
||||||
"""
|
"""
|
||||||
model_class = HarvestCacheTimeEntry
|
model_class = HarvestTimeEntry
|
||||||
url_prefix = '/harvest/time-entries'
|
url_prefix = '/harvest/time-entries'
|
||||||
route_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 = [
|
grid_columns = [
|
||||||
'id',
|
'id',
|
||||||
'spent_date',
|
'spent_date',
|
||||||
|
@ -50,7 +57,7 @@ class HarvestCacheTimeEntryView(HarvestMasterView):
|
||||||
]
|
]
|
||||||
|
|
||||||
def configure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
super().configure_grid(g)
|
super(HarvestTimeEntryView, self).configure_grid(g)
|
||||||
|
|
||||||
g.set_type('hours', 'duration_hours')
|
g.set_type('hours', 'duration_hours')
|
||||||
|
|
||||||
|
@ -61,103 +68,6 @@ class HarvestCacheTimeEntryView(HarvestMasterView):
|
||||||
g.set_link('client')
|
g.set_link('client')
|
||||||
g.set_link('notes')
|
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):
|
def includeme(config):
|
||||||
defaults(config)
|
HarvestTimeEntryView.defaults(config)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2023 Lance Edgar
|
# Copyright © 2010-2022 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -24,27 +24,19 @@
|
||||||
Harvest User views
|
Harvest User views
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from rattail_harvest.db.model import HarvestCacheUser
|
from rattail_harvest.db.model import HarvestUser
|
||||||
from rattail_harvest.harvest.config import get_harvest_url
|
|
||||||
|
|
||||||
import colander
|
|
||||||
|
|
||||||
from tailbone import forms
|
|
||||||
from .master import HarvestMasterView
|
from .master import HarvestMasterView
|
||||||
|
|
||||||
|
|
||||||
class HarvestCacheUserView(HarvestMasterView):
|
class HarvestUserView(HarvestMasterView):
|
||||||
"""
|
"""
|
||||||
Master view for Harvest Users
|
Master view for Harvest Users
|
||||||
"""
|
"""
|
||||||
model_class = HarvestCacheUser
|
model_class = HarvestUser
|
||||||
url_prefix = '/harvest/users'
|
url_prefix = '/harvest/users'
|
||||||
route_prefix = 'harvest.users'
|
route_prefix = 'harvest.users'
|
||||||
|
|
||||||
labels = {
|
|
||||||
'avatar_url': "Avatar URL",
|
|
||||||
}
|
|
||||||
|
|
||||||
grid_columns = [
|
grid_columns = [
|
||||||
'id',
|
'id',
|
||||||
'first_name',
|
'first_name',
|
||||||
|
@ -56,11 +48,7 @@ class HarvestCacheUserView(HarvestMasterView):
|
||||||
]
|
]
|
||||||
|
|
||||||
def configure_grid(self, g):
|
def configure_grid(self, g):
|
||||||
super().configure_grid(g)
|
super(HarvestUserView, self).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')
|
g.set_sort_defaults('first_name')
|
||||||
|
|
||||||
|
@ -70,75 +58,11 @@ class HarvestCacheUserView(HarvestMasterView):
|
||||||
g.set_link('email')
|
g.set_link('email')
|
||||||
|
|
||||||
def configure_form(self, f):
|
def configure_form(self, f):
|
||||||
super().configure_form(f)
|
super(HarvestUserView, self).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
|
# TODO: should add this as child rows/grid instead
|
||||||
f.remove('time_entries')
|
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):
|
def includeme(config):
|
||||||
defaults(config)
|
HarvestUserView.defaults(config)
|
||||||
|
|
26
tasks.py
26
tasks.py
|
@ -2,7 +2,7 @@
|
||||||
################################################################################
|
################################################################################
|
||||||
#
|
#
|
||||||
# Rattail -- Retail Software Framework
|
# Rattail -- Retail Software Framework
|
||||||
# Copyright © 2010-2024 Lance Edgar
|
# Copyright © 2010-2022 Lance Edgar
|
||||||
#
|
#
|
||||||
# This file is part of Rattail.
|
# This file is part of Rattail.
|
||||||
#
|
#
|
||||||
|
@ -25,36 +25,24 @@ Tasks for tailbone-harvest
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
from invoke import task
|
from invoke import task
|
||||||
|
|
||||||
|
|
||||||
here = os.path.abspath(os.path.dirname(__file__))
|
here = os.path.abspath(os.path.dirname(__file__))
|
||||||
__version__ = None
|
exec(open(os.path.join(here, 'tailbone_harvest', '_version.py')).read())
|
||||||
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
|
@task
|
||||||
def release(c):
|
def release(ctx):
|
||||||
"""
|
"""
|
||||||
Release a new version of tailbone-harvest
|
Release a new version of tailbone-harvest
|
||||||
"""
|
"""
|
||||||
# rebuild local tar.gz file for distribution
|
# rebuild local tar.gz file for distribution
|
||||||
if os.path.exists('tailbone_harvest.egg-info'):
|
shutil.rmtree('tailbone_harvest.egg-info')
|
||||||
shutil.rmtree('tailbone_harvest.egg-info')
|
ctx.run('python setup.py sdist --formats=gztar')
|
||||||
c.run('python -m build --sdist')
|
|
||||||
|
|
||||||
# upload to public PyPI
|
# upload to public PyPI
|
||||||
filename = f'tailbone_harvest-{__version__}.tar.gz'
|
filename = 'tailbone-harvest-{}.tar.gz'.format(__version__)
|
||||||
c.run(f'twine upload dist/{filename}')
|
ctx.run('twine upload dist/{}'.format(filename))
|
||||||
|
|
Loading…
Reference in a new issue