Skip to content

Commit

Permalink
Billability changes:
Browse files Browse the repository at this point in the history
- Billable flag on projects in Toggl Track is ignored, only looks at entries
- Rename `--unbillable` flag to `--include-unbillable`
- Documentation

Fixes #4
  • Loading branch information
rdb committed Jul 21, 2024
1 parent e2ff526 commit 6e1f731
Show file tree
Hide file tree
Showing 5 changed files with 47 additions and 14 deletions.
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ automatically making changes to your administration.
toggl2moneybird sync

Only time entries corresponding to a project are synced. By default, only
billable items are synced, but you can add the `--unbillable` flag to include
unbillable items as well.
billable items are synced, but you can add the `--include-unbillable` flag to
include unbillable items as well.

You can also automatically create a draft invoice for a particular contact and
period using the following command:
Expand All @@ -35,6 +35,25 @@ period using the following command:
The invoice will not be sent out automatically. Instead, a browser window will
open with the draft invoice in Moneybird, allowing you to send it from there.

## Billable flag

By default, only entries marked "Billable" in Toggl Track are synchronized.
There are two options to control this behaviour. The `--include-unbillable`
flag will cause all entries to be synchronized. The "Billable" flag is only
set in moneybird for entries that are marked "Billable" in Toggl Track.

Since the "Billable" tag requires a paid Toggl Track subscription, there is an
option to use a custom tag instead. If you use `--unbillable-tag "My Tag"`,
then all Toggl Track entries will be considered billable, except those with
the tag "My Tag". The option may be repeated for every tag that marks an
unbillable entry. If you would like to mark all imported entries billable in
moneybird, simply use this option with a silly tag name that doesn't exist.

The options are independent of each other and may be used together, in which
case *all* entries (except those specified by `--exclude-tag`) are imported,
but only the ones without the tags specified by `--unbillable-tag` are marked
as "Billable" in the moneybird administration.

## Limitations

The Toggl Track API only allows accessing the last three months worth of data.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "toggl2moneybird"
version = "0.4.1"
version = "0.5.0"
authors = [
{name = "rdb", email = "[email protected]"},
]
Expand Down
6 changes: 4 additions & 2 deletions toggl2moneybird/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import keyring
import getpass
from datetime import datetime, date
import argparse
from argparse import ArgumentParser

from rich.console import Console
Expand All @@ -29,8 +30,9 @@ def main():
parser.add_argument('-y', action='store_true', dest='yes', help="Do not ask for confirmation")
parser.add_argument('--project', action='append', metavar='"Project"', dest='projects', help="Limit to the given project (may be repeated)")
parser.add_argument('--exclude-tag', action='append', metavar='"tag"', dest='exclude_tags', help="Exclude entries with the given tag (may be repeated)")
parser.add_argument('--unbillable', action='store_false', dest='only_billable', help="Include unbillable entries")
parser.add_argument('--unbillable-tag', action='store', dest='unbillable_tag', help="Consider all entries billable except those with the given tag")
parser.add_argument('--include-unbillable', action='store_false', dest='only_billable', help="Include entries which are not marked billable in Toggl Track")
parser.add_argument('--unbillable', action='store_false', dest='only_billable', help=argparse.SUPPRESS)
parser.add_argument('--unbillable-tag', action='store', dest='unbillable_tag', help="Consider all Toggl Track entries billable except those with the given tag")
parser.add_argument('--rate', action='store', dest='rate', type=float, help="Use the given rate for invoices, overrides rate in Toggl Track")
parser.add_argument('--currency', action='store', dest='currency', help="Use the given currency for invoices (e.g. EUR), overrides currency in Toggl Track")
args = parser.parse_args()
Expand Down
6 changes: 5 additions & 1 deletion toggl2moneybird/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,8 @@ def cmd_sync(console, args, mb_admin):
if not args.projects or tt_project['name'] in args.projects:
if args.unbillable_tag:
tt_entry['billable'] = args.unbillable_tag not in tt_entry['tags']
if args.only_billable and not tt_entry['billable']:
continue
sync.add_tt_entry(tt_entry, tt_project)

with Progress(console=console, transient=True) as progress:
Expand Down Expand Up @@ -388,7 +390,7 @@ def cmd_sync(console, args, mb_admin):
do_mutations(console, args, mb_admin, mutations)

if not mutations and args.only_billable and sync.has_missing_projects(False):
console.print("To include unbillable projects, add the [bold]--unbillable[/bold] flag.")
console.print("To include unbillable entries, add the [bold]--include-unbillable[/bold] flag.")


def cmd_invoice(console, args, mb_admin):
Expand Down Expand Up @@ -432,6 +434,8 @@ def cmd_invoice(console, args, mb_admin):
if not args.projects or tt_project['name'] in args.projects:
if args.unbillable_tag:
tt_entry['billable'] = args.unbillable_tag not in tt_entry['tags']
if args.only_billable and not tt_entry['billable']:
continue
sync.add_tt_entry(tt_entry, tt_project)

sync.link(entries)
Expand Down
24 changes: 16 additions & 8 deletions toggl2moneybird/sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def __init__(self):
self.__entry_map = {}
self.__project_map: dict[int, mb.Project] = {}
self.__missing_projects: dict[int, dict] = {}
self.__missing_projects_billable = set()
self.__mb_project_contact_map: dict[mb.Project, mb.Contact] = {}
self.__mb_redundant_entries = set()

Expand Down Expand Up @@ -65,6 +66,8 @@ def add_tt_entry(self, tt_entry, tt_project):
entry.mb_project = self.__project_map.get(tt_project_id)
if not entry.mb_project:
self.__missing_projects[tt_project_id] = tt_project
if tt_entry['billable']:
self.__missing_projects_billable.add(tt_project_id)

entry.stop = tt_parse_timestamp(tt_entry['stop'])

Expand Down Expand Up @@ -101,8 +104,10 @@ def link(self, mb_entries):
tt_project = entry.tt_project
if mb_project:
if tt_project and tt_project['id'] not in self.__project_map:
self.__project_map[tt_project['id']] = entry.mb_project
self.__missing_projects.pop(tt_project['id'], None)
tt_project_id = tt_project['id']
self.__project_map[tt_project_id] = entry.mb_project
self.__missing_projects.pop(tt_project_id, None)
self.__missing_projects_billable.discard(tt_project_id)

if mb_contact:
self.__mb_project_contact_map[mb_project] = mb_contact
Expand All @@ -127,20 +132,22 @@ def map_project(self, tt_project_id, mb_project):
assert mb_project
self.__project_map[tt_project_id] = mb_project
self.__missing_projects.pop(tt_project_id, None)
self.__missing_projects_billable.discard(tt_project_id)

for entry in self.entries:
if entry.tt_project and entry.tt_project['id'] == tt_project_id:
entry.mb_project = mb_project

def has_missing_projects(self, only_billable=False):
if not only_billable:
#NB. We used to look at the billable flag on a project, but this
# doesn't actually say anything about whether the entries in the
# project are billable or not, so I decided to only look at whether
# the project has billable entries. See GitHub issue #4.
if only_billable:
return bool(self.__missing_projects_billable)
else:
return bool(self.__missing_projects)

for proj in self.__missing_projects.values():
if proj['billable']:
return True
return False

def map_projects_by_name(self, mb_projects):
if not self.entries or not self.__missing_projects:
return
Expand All @@ -160,6 +167,7 @@ def map_projects_by_name(self, mb_projects):
entry.mb_project = mb_project
self.__project_map[tt_project_id] = mb_project
self.__missing_projects.pop(tt_project_id, None)
self.__missing_projects_billable.discard(tt_project_id)

return tuple(self.__missing_projects.values())

Expand Down

0 comments on commit 6e1f731

Please sign in to comment.