emacs-orgmode@gnu.org archives
 help / color / mirror / code / Atom feed
From: "Antoine Beaupré" <anarcat@debian.org>
To: emacs-orgmode@gnu.org
Cc: "Antoine Beaupré" <anarcat@debian.org>
Subject: [PATCH] import org2tc scripts from John Wiegly into org-mode
Date: Fri, 20 Jan 2017 13:18:06 -0500	[thread overview]
Message-ID: <20170120181806.7332-1-anarcat@debian.org> (raw)

this was taken from this Github repo with the author's approval:

https://github.com/jwiegley/org2tc

this is very useful to convert org-mode clock entries into the more
easily parseable timeclock.el format, a fundamental step in automating
billing with org-mode.
---
 contrib/scripts/org2tc | 150 +++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 150 insertions(+)
 create mode 100755 contrib/scripts/org2tc

diff --git a/contrib/scripts/org2tc b/contrib/scripts/org2tc
new file mode 100755
index 000000000..9ff6422d7
--- /dev/null
+++ b/contrib/scripts/org2tc
@@ -0,0 +1,150 @@
+#!/usr/bin/python
+
+'''Take an org-mode file as input and print a timeclock file as
+output. This can then be read directly by Ledger for fancy time
+reporting and querying. Fields :BILLCODE: and :TASKCODE: are parsed to
+generate lines compatible with the format expected by ledger
+("billcode taskcode").
+
+See also http://ledger-cli.org/2.6/ledger.html#Using-timeclock-to-record-billable-time
+
+© 2011-2016 John Wiegly 
+© 2016-2017 Antoine Beaupré
+'''
+
+
+from __future__ import print_function
+
+import argparse
+import locale
+locale.setlocale(locale.LC_ALL, '')
+import sys
+import re
+import time
+
+iso_date_fmt = "%Y-%m-%d %H:%M:%S"
+
+def parse_org_time(s):
+    return time.strptime(s, "%Y-%m-%d %a %H:%M")
+
+def parse_timestamp(s):
+    return time.strptime(s, iso_date_fmt)
+
+events       = []
+last_heading = None
+clocks       = []
+
+parser = argparse.ArgumentParser(description='convert org clocks into timeclock',
+                                 epilog=__doc__ +
+                                 '''Note that TIME is provided in the following format: %s'''
+                                 % iso_date_fmt)
+parser.add_argument('orgfile', help='Org file to process')
+parser.add_argument('-s', '--start', metavar='TIME', help='process only entries from this date')
+parser.add_argument('-e', '--end', metavar='TIME', help='process only entries to this date')
+parser.add_argument('-r', '--regex', help='process only entries matching this regex')
+parser.add_argument('-o', '--output', help='output file (default: stdout)',
+                    type=argparse.FileType('w'), default=sys.stdout)
+args = parser.parse_args()
+
+data         = args.orgfile
+range_start  = parse_timestamp(args.start) if args.start else None
+range_end    = parse_timestamp(args.end) if args.end else None
+regex        = args.regex
+fd           = open(data, "r")
+headings     = [None] * 9
+acct         = "<None>"
+
+(billcode, taskcode) = ("<Unknown>", None)
+
+def add_events():
+    # XXX: those globals should really be cleaned up, maybe through a clock object or named tuple
+    global acct, clocks, billcode, taskcode, events, todo_keyword, last_heading
+    if clocks:
+        for (clock_in, clock_out, billcode, taskcode) in clocks:
+            if billcode and ":" not in billcode and taskcode:
+                acct = "%s:%s" % (billcode, taskcode)
+            events.append((clock_in, clock_out, todo_keyword,
+                           ("%s  %s" % (acct, last_heading))
+                           if acct else last_heading))
+        clocks = []
+
+for line in fd:
+    match = re.search("^(\*+)\s*(.+)", line)
+    if match:
+        depth = len(match.group(1))
+        headings[depth] = match.group(2)
+
+    depth = 0
+    match = re.search("^(\*+)\s+(TODO|DONE)?(\s+\[#[ABC]\])?\s*(.+)", line)
+    if match:
+        add_events()
+
+        depth = len(match.group(1))
+        todo_keyword = match.group(2)
+        last_heading = match.group(4)
+        match = re.search("(.+?)\s+:\S+:$", last_heading)
+        if match:
+            last_heading = match.group(1)
+        match = re.search("\[\[.*\]\]\s+(.+?)$", last_heading)
+        if match:
+            last_heading = match.group(1)
+
+        headings[depth] = last_heading
+
+        i = 0
+        prefix = ""
+        while i < depth:
+            if prefix:
+                prefix += ":" + headings[i]
+            else:
+                prefix = headings[i]
+            i += 1
+
+        if prefix:
+            #last_heading = prefix + "  " + last_heading
+            last_heading = prefix + ":" + last_heading
+
+        if regex and not (prefix and re.search(regex, prefix)):
+            last_heading = None
+
+    if last_heading:
+        match = re.search("CLOCK:\s+\[(.+?)\](--\[(.+?)\])?", line)
+        if match:
+            clock_in  = parse_org_time(match.group(1))
+            clock_out = match.group(3) # optional
+            if clock_out:
+                clock_out = parse_org_time(clock_out)
+            else:
+                #clock_out = time.localtime()
+                clock_out = None
+            if (not range_start or clock_in >= range_start) and \
+               (not range_end or clock_in < range_end):
+               clocks.append((clock_in, clock_out, billcode, taskcode))
+            elif clock_in < range_start and clock_out > range_start:
+               clocks.append((range_start, clock_out, billcode, taskcode))
+            elif clock_in < range_end and clock_out > range_end:
+               clocks.append((clock_in, range_end, billcode, taskcode))
+
+        match = re.search(":BILLCODE:\s+(.+)", line)
+        if match:
+            billcode = match.group(1)
+            taskcode = None
+
+        match = re.search(":TASKCODE:\s+(.+)", line)
+        if match:
+            taskcode = match.group(1)
+
+fd.close()
+add_events()
+
+events.sort(key=lambda x: time.mktime(x[0]))
+
+for event in events:
+    print("i %s %s" % (time.strftime(iso_date_fmt, event[0]), event[3]),
+          file=args.output)
+    if event[1]:
+        print("%s %s" % ('O' if event[2] == 'DONE' else 'o',
+                         time.strftime(iso_date_fmt, event[1])),
+              file=args.output)
+
+# org2tc ends here
-- 
2.11.0

             reply	other threads:[~2017-01-20 18:18 UTC|newest]

Thread overview: 3+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2017-01-20 18:18 Antoine Beaupré [this message]
2017-01-22 13:13 ` [PATCH] import org2tc scripts from John Wiegly into org-mode Nicolas Goaziou
2017-01-22 19:30   ` Antoine Beaupré

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

  List information: https://www.orgmode.org/

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20170120181806.7332-1-anarcat@debian.org \
    --to=anarcat@debian.org \
    --cc=emacs-orgmode@gnu.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
Code repositories for project(s) associated with this public inbox

	https://git.savannah.gnu.org/cgit/emacs/org-mode.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).