Skip to main content
Bounty Ended with 50 reputation awarded by Nagora
Just formatting the source code as elisp.
Source Link
Tobias
  • 33.7k
  • 1
  • 38
  • 78
(defun dfeich/org-clock-get-tr-for-ivl (buffer tstart-str tend-str &optional limit)
  "Return clocking information touching a given time interval."
  (cl-assert (and buffer (get-buffer buffer)) nil "Error: :buffer must be defined")
  (with-current-buffer buffer
    (save-excursion
      (let ((re (concat "^\\(\\*+[ \t]*.*\\)\\|^[ \t]*"
            org-clock-string
            "[ \t]*\\(?:\\(\\[.*?\\]\\)-+\\(\\[.*?\\]\\)\\|=>[ \t]+\\([0-9]+\\):\\([0-9]+\\)\\)"))
        (counter 0)
        (tmphd "BEFORE FIRST HEADING")
        (tstart (org-time-string-to-seconds tstart-str))
        (tend (org-time-string-to-seconds tend-str))
        (limit (or limit (point-max)))
        headings timelst
        lvl title result ts te)
    (goto-char (point-min))
    (cl-block myblock
      (while (re-search-forward re nil t)
        (cond
         ;; found a org heading
         ((match-end 1)
          (if (> (length timelst) 0)
          (setq result (nconc result (list (list
                            (copy-sequence headings)
                            timelst)))))
          (setq tmphd (org-heading-components)
            lvl (car tmphd)
            title (nth 4 tmphd)
            timelst nil)
          ;; maintain a list of the current heading hierarchy
          (cond
           ((> lvl (length headings))
        (setq headings  (nconc headings `(,title))))
           ((= lvl (length headings))
        (setf (nth (1- lvl) headings) title))
           ((< lvl (length headings))
        (setq headings (cl-subseq headings 0 lvl))
        (setf (nth (1- lvl) headings) title))))
         ;; found a clock line with 2 timestamps
         ((match-end 3)
          (setq ts (save-match-data (org-time-string-to-seconds
                     (match-string-no-properties 2)))
            te (save-match-data (org-time-string-to-seconds
                     (match-string-no-properties 3))))
          ;; the clock lines progress from newest to oldest. This
          ;; enables skipping the rest if this condition is true
          (if (> tstart te)
          (if (re-search-forward "^\\(\\*+[ \t]*.*\\)" nil t)
              (beginning-of-line)
            (goto-char (point-max)))
        (when (> tend ts)
          (setq timelst (nconc timelst (list
                        (list (match-string-no-properties 2)
                              (match-string-no-properties 3)))))))))
        (when (>= (point) limit)
          (cl-return-from myblock))))
    (if (> (length timelst) 0)
        (setq result (nconc result (list (list (copy-sequence headings)
                           timelst)))))
    result))))

(defun dfeich/org-slice-tr (tstart-str tend-str cutstart-str cutend-str)
  "Return time slice of a time range in minutes."
  (let ((tstart (org-time-string-to-seconds tstart-str))
    (tend (org-time-string-to-seconds tend-str))
    (cutstart (if (stringp cutstart-str)
              (org-time-string-to-seconds cutstart-str)
            cutstart-str))
    (cutend (if (stringp cutend-str)
            (org-time-string-to-seconds cutend-str)
          cutend-str))
    result)
    (setq result (max 0
              (/  (- (min tend cutend) (max tstart cutstart))
              60)))))

(defun dfeich/org-clock-hourly-report (struct tstart-str tend-str)
  "Return a structure containing a per hour report within an interval."
  (let* ((tstart (org-time-string-to-seconds tstart-str))
     (tend (org-time-string-to-seconds tend-str))
     (delta 3600)
     (intvls (cl-loop for tm from tstart to (- tend delta) by delta
              collect `(,tm ,(+ tm delta))))
     result)
    ;; iterate over the intervals for the final table
    (cl-loop for iv in intvls
         collect (list
              iv
              (let* ((cutstart (car iv))
                 (cutend (cadr iv))
                 (tmsum 0.0)
                 headings trlst)
            ;; iterate over the task structure
            (cl-loop
             for item in struct
             do (progn
                  (setq headings (car item)
                    trlst (cadr item)
                    ;; sum up the parts of the time
                    ;; ranges falling into this
                    ;; interval
                    tmsum (apply
                       #'+
                       (mapcar
                        (lambda (tr)
                          (dfeich/org-slice-tr (car tr)
                                   (cadr tr)
                                   cutstart
                                   cutend))
                        trlst))))
             if (> tmsum 0) collect `(,headings ,tmsum) into lst
             finally return lst))))))

(defun org-dblock-write:nagora-report (params)
 "Fill in a dynamic timesheet reporting block."
  (let* ((buffer (plist-get params :buffer))
     (day (symbol-name (plist-get params :day)))
     (tstart (if (string-match-p "^[0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}$" day)
             day
           (error "Error: day format must be in YYYY-mm-dd format")))
     (tend (concat day " 23:59"))
     (table (dfeich/org-clock-hourly-report
         (dfeich/org-clock-get-tr-for-ivl buffer tstart tend)
         tstart tend)))
    (insert (format "#+CAPTION: timesheet for day %s\n" day))
    (insert "|Time|Customer| Task |Minutes|\n|------\n")
    (cl-loop
     for item in table
     do (let ((ivl (car item))
          (entries (cadr item)))
      (cl-loop for e in entries
           do (let ((headings (car e))
                (minutes (cadr e)))
            (insert (concat
                 "|"
                 (format-time-string "%H:%M" (seconds-to-time
                                  (car ivl)))
                 "-"
                 (format-time-string "%H:%M" (seconds-to-time
                                  (cadr ivl)))
                 "|" (nth 1 headings)
                 "|" (car (last headings))
                 "|" (format "%d" minutes)
                 "|\n"))))))
    (insert "|----\n|TOTAL||||\n#+TBLFM: @>$>=vsum(@I..@II)")
    (search-backward "Time")
    (org-table-align)
    (org-table-recalculate '(16))))
(defun dfeich/org-clock-get-tr-for-ivl (buffer tstart-str tend-str &optional limit)
  "Return clocking information touching a given time interval."
  (cl-assert (and buffer (get-buffer buffer)) nil "Error: :buffer must be defined")
  (with-current-buffer buffer
    (save-excursion
      (let ((re (concat "^\\(\\*+[ \t]*.*\\)\\|^[ \t]*"
            org-clock-string
            "[ \t]*\\(?:\\(\\[.*?\\]\\)-+\\(\\[.*?\\]\\)\\|=>[ \t]+\\([0-9]+\\):\\([0-9]+\\)\\)"))
        (counter 0)
        (tmphd "BEFORE FIRST HEADING")
        (tstart (org-time-string-to-seconds tstart-str))
        (tend (org-time-string-to-seconds tend-str))
        (limit (or limit (point-max)))
        headings timelst
        lvl title result ts te)
    (goto-char (point-min))
    (cl-block myblock
      (while (re-search-forward re nil t)
        (cond
         ;; found a org heading
         ((match-end 1)
          (if (> (length timelst) 0)
          (setq result (nconc result (list (list
                            (copy-sequence headings)
                            timelst)))))
          (setq tmphd (org-heading-components)
            lvl (car tmphd)
            title (nth 4 tmphd)
            timelst nil)
          ;; maintain a list of the current heading hierarchy
          (cond
           ((> lvl (length headings))
        (setq headings  (nconc headings `(,title))))
           ((= lvl (length headings))
        (setf (nth (1- lvl) headings) title))
           ((< lvl (length headings))
        (setq headings (cl-subseq headings 0 lvl))
        (setf (nth (1- lvl) headings) title))))
         ;; found a clock line with 2 timestamps
         ((match-end 3)
          (setq ts (save-match-data (org-time-string-to-seconds
                     (match-string-no-properties 2)))
            te (save-match-data (org-time-string-to-seconds
                     (match-string-no-properties 3))))
          ;; the clock lines progress from newest to oldest. This
          ;; enables skipping the rest if this condition is true
          (if (> tstart te)
          (if (re-search-forward "^\\(\\*+[ \t]*.*\\)" nil t)
              (beginning-of-line)
            (goto-char (point-max)))
        (when (> tend ts)
          (setq timelst (nconc timelst (list
                        (list (match-string-no-properties 2)
                              (match-string-no-properties 3)))))))))
        (when (>= (point) limit)
          (cl-return-from myblock))))
    (if (> (length timelst) 0)
        (setq result (nconc result (list (list (copy-sequence headings)
                           timelst)))))
    result))))

(defun dfeich/org-slice-tr (tstart-str tend-str cutstart-str cutend-str)
  "Return time slice of a time range in minutes."
  (let ((tstart (org-time-string-to-seconds tstart-str))
    (tend (org-time-string-to-seconds tend-str))
    (cutstart (if (stringp cutstart-str)
              (org-time-string-to-seconds cutstart-str)
            cutstart-str))
    (cutend (if (stringp cutend-str)
            (org-time-string-to-seconds cutend-str)
          cutend-str))
    result)
    (setq result (max 0
              (/  (- (min tend cutend) (max tstart cutstart))
              60)))))

(defun dfeich/org-clock-hourly-report (struct tstart-str tend-str)
  "Return a structure containing a per hour report within an interval."
  (let* ((tstart (org-time-string-to-seconds tstart-str))
     (tend (org-time-string-to-seconds tend-str))
     (delta 3600)
     (intvls (cl-loop for tm from tstart to (- tend delta) by delta
              collect `(,tm ,(+ tm delta))))
     result)
    ;; iterate over the intervals for the final table
    (cl-loop for iv in intvls
         collect (list
              iv
              (let* ((cutstart (car iv))
                 (cutend (cadr iv))
                 (tmsum 0.0)
                 headings trlst)
            ;; iterate over the task structure
            (cl-loop
             for item in struct
             do (progn
                  (setq headings (car item)
                    trlst (cadr item)
                    ;; sum up the parts of the time
                    ;; ranges falling into this
                    ;; interval
                    tmsum (apply
                       #'+
                       (mapcar
                        (lambda (tr)
                          (dfeich/org-slice-tr (car tr)
                                   (cadr tr)
                                   cutstart
                                   cutend))
                        trlst))))
             if (> tmsum 0) collect `(,headings ,tmsum) into lst
             finally return lst))))))

(defun org-dblock-write:nagora-report (params)
 "Fill in a dynamic timesheet reporting block."
  (let* ((buffer (plist-get params :buffer))
     (day (symbol-name (plist-get params :day)))
     (tstart (if (string-match-p "^[0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}$" day)
             day
           (error "Error: day format must be in YYYY-mm-dd format")))
     (tend (concat day " 23:59"))
     (table (dfeich/org-clock-hourly-report
         (dfeich/org-clock-get-tr-for-ivl buffer tstart tend)
         tstart tend)))
    (insert (format "#+CAPTION: timesheet for day %s\n" day))
    (insert "|Time|Customer| Task |Minutes|\n|------\n")
    (cl-loop
     for item in table
     do (let ((ivl (car item))
          (entries (cadr item)))
      (cl-loop for e in entries
           do (let ((headings (car e))
                (minutes (cadr e)))
            (insert (concat
                 "|"
                 (format-time-string "%H:%M" (seconds-to-time
                                  (car ivl)))
                 "-"
                 (format-time-string "%H:%M" (seconds-to-time
                                  (cadr ivl)))
                 "|" (nth 1 headings)
                 "|" (car (last headings))
                 "|" (format "%d" minutes)
                 "|\n"))))))
    (insert "|----\n|TOTAL||||\n#+TBLFM: @>$>=vsum(@I..@II)")
    (search-backward "Time")
    (org-table-align)
    (org-table-recalculate '(16))))
(defun dfeich/org-clock-get-tr-for-ivl (buffer tstart-str tend-str &optional limit)
  "Return clocking information touching a given time interval."
  (cl-assert (and buffer (get-buffer buffer)) nil "Error: :buffer must be defined")
  (with-current-buffer buffer
    (save-excursion
      (let ((re (concat "^\\(\\*+[ \t]*.*\\)\\|^[ \t]*"
            org-clock-string
            "[ \t]*\\(?:\\(\\[.*?\\]\\)-+\\(\\[.*?\\]\\)\\|=>[ \t]+\\([0-9]+\\):\\([0-9]+\\)\\)"))
        (counter 0)
        (tmphd "BEFORE FIRST HEADING")
        (tstart (org-time-string-to-seconds tstart-str))
        (tend (org-time-string-to-seconds tend-str))
        (limit (or limit (point-max)))
        headings timelst
        lvl title result ts te)
    (goto-char (point-min))
    (cl-block myblock
      (while (re-search-forward re nil t)
        (cond
         ;; found a org heading
         ((match-end 1)
          (if (> (length timelst) 0)
          (setq result (nconc result (list (list
                            (copy-sequence headings)
                            timelst)))))
          (setq tmphd (org-heading-components)
            lvl (car tmphd)
            title (nth 4 tmphd)
            timelst nil)
          ;; maintain a list of the current heading hierarchy
          (cond
           ((> lvl (length headings))
        (setq headings  (nconc headings `(,title))))
           ((= lvl (length headings))
        (setf (nth (1- lvl) headings) title))
           ((< lvl (length headings))
        (setq headings (cl-subseq headings 0 lvl))
        (setf (nth (1- lvl) headings) title))))
         ;; found a clock line with 2 timestamps
         ((match-end 3)
          (setq ts (save-match-data (org-time-string-to-seconds
                     (match-string-no-properties 2)))
            te (save-match-data (org-time-string-to-seconds
                     (match-string-no-properties 3))))
          ;; the clock lines progress from newest to oldest. This
          ;; enables skipping the rest if this condition is true
          (if (> tstart te)
          (if (re-search-forward "^\\(\\*+[ \t]*.*\\)" nil t)
              (beginning-of-line)
            (goto-char (point-max)))
        (when (> tend ts)
          (setq timelst (nconc timelst (list
                        (list (match-string-no-properties 2)
                              (match-string-no-properties 3)))))))))
        (when (>= (point) limit)
          (cl-return-from myblock))))
    (if (> (length timelst) 0)
        (setq result (nconc result (list (list (copy-sequence headings)
                           timelst)))))
    result))))

(defun dfeich/org-slice-tr (tstart-str tend-str cutstart-str cutend-str)
  "Return time slice of a time range in minutes."
  (let ((tstart (org-time-string-to-seconds tstart-str))
    (tend (org-time-string-to-seconds tend-str))
    (cutstart (if (stringp cutstart-str)
              (org-time-string-to-seconds cutstart-str)
            cutstart-str))
    (cutend (if (stringp cutend-str)
            (org-time-string-to-seconds cutend-str)
          cutend-str))
    result)
    (setq result (max 0
              (/  (- (min tend cutend) (max tstart cutstart))
              60)))))

(defun dfeich/org-clock-hourly-report (struct tstart-str tend-str)
  "Return a structure containing a per hour report within an interval."
  (let* ((tstart (org-time-string-to-seconds tstart-str))
     (tend (org-time-string-to-seconds tend-str))
     (delta 3600)
     (intvls (cl-loop for tm from tstart to (- tend delta) by delta
              collect `(,tm ,(+ tm delta))))
     result)
    ;; iterate over the intervals for the final table
    (cl-loop for iv in intvls
         collect (list
              iv
              (let* ((cutstart (car iv))
                 (cutend (cadr iv))
                 (tmsum 0.0)
                 headings trlst)
            ;; iterate over the task structure
            (cl-loop
             for item in struct
             do (progn
                  (setq headings (car item)
                    trlst (cadr item)
                    ;; sum up the parts of the time
                    ;; ranges falling into this
                    ;; interval
                    tmsum (apply
                       #'+
                       (mapcar
                        (lambda (tr)
                          (dfeich/org-slice-tr (car tr)
                                   (cadr tr)
                                   cutstart
                                   cutend))
                        trlst))))
             if (> tmsum 0) collect `(,headings ,tmsum) into lst
             finally return lst))))))

(defun org-dblock-write:nagora-report (params)
 "Fill in a dynamic timesheet reporting block."
  (let* ((buffer (plist-get params :buffer))
     (day (symbol-name (plist-get params :day)))
     (tstart (if (string-match-p "^[0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}$" day)
             day
           (error "Error: day format must be in YYYY-mm-dd format")))
     (tend (concat day " 23:59"))
     (table (dfeich/org-clock-hourly-report
         (dfeich/org-clock-get-tr-for-ivl buffer tstart tend)
         tstart tend)))
    (insert (format "#+CAPTION: timesheet for day %s\n" day))
    (insert "|Time|Customer| Task |Minutes|\n|------\n")
    (cl-loop
     for item in table
     do (let ((ivl (car item))
          (entries (cadr item)))
      (cl-loop for e in entries
           do (let ((headings (car e))
                (minutes (cadr e)))
            (insert (concat
                 "|"
                 (format-time-string "%H:%M" (seconds-to-time
                                  (car ivl)))
                 "-"
                 (format-time-string "%H:%M" (seconds-to-time
                                  (cadr ivl)))
                 "|" (nth 1 headings)
                 "|" (car (last headings))
                 "|" (format "%d" minutes)
                 "|\n"))))))
    (insert "|----\n|TOTAL||||\n#+TBLFM: @>$>=vsum(@I..@II)")
    (search-backward "Time")
    (org-table-align)
    (org-table-recalculate '(16))))
(defun dfeich/org-clock-get-tr-for-ivl (buffer tstart-str tend-str &optional limit)
  "Return clocking information touching a given time interval."
  (cl-assert (and buffer (get-buffer buffer)) nil "Error: :buffer must be defined")
  (with-current-buffer buffer
    (save-excursion
      (let ((re (concat "^\\(\\*+[ \t]*.*\\)\\|^[ \t]*"
            org-clock-string
            "[ \t]*\\(?:\\(\\[.*?\\]\\)-+\\(\\[.*?\\]\\)\\|=>[ \t]+\\([0-9]+\\):\\([0-9]+\\)\\)"))
        (counter 0)
        (tmphd "BEFORE FIRST HEADING")
        (tstart (org-time-string-to-seconds tstart-str))
        (tend (org-time-string-to-seconds tend-str))
        (limit (or limit (point-max)))
        headings timelst
        lvl title result ts te)
    (goto-char (point-min))
    (cl-block myblock
      (while (re-search-forward re nil t)
        (cond
         ;; found a org heading
         ((match-end 1)
          (if (> (length timelst) 0)
          (setq result (nconc result (list (list
                            (copy-sequence headings)
                            timelst)))))
          (setq tmphd (org-heading-components)
            lvl (car tmphd)
            title (nth 4 tmphd)
            timelst nil)
          ;; maintain a list of the current heading hierarchy
          (cond
           ((> lvl (length headings))
        (setq headings  (nconc headings `(,title))))
           ((= lvl (length headings))
        (setf (nth (1- lvl) headings) title))
           ((< lvl (length headings))
        (setq headings (cl-subseq headings 0 lvl))
        (setf (nth (1- lvl) headings) title))))
         ;; found a clock line with 2 timestamps
         ((match-end 3)
          (setq ts (save-match-data (org-time-string-to-seconds
                     (match-string-no-properties 2)))
            te (save-match-data (org-time-string-to-seconds
                     (match-string-no-properties 3))))
          ;; the clock lines progress from newest to oldest. This
          ;; enables skipping the rest if this condition is true
          (if (> tstart te)
          (if (re-search-forward "^\\(\\*+[ \t]*.*\\)" nil t)
              (beginning-of-line)
            (goto-char (point-max)))
        (when (> tend ts)
          (setq timelst (nconc timelst (list
                        (list (match-string-no-properties 2)
                              (match-string-no-properties 3)))))))))
        (when (>= (point) limit)
          (cl-return-from myblock))))
    (if (> (length timelst) 0)
        (setq result (nconc result (list (list (copy-sequence headings)
                           timelst)))))
    result))))

(defun dfeich/org-slice-tr (tstart-str tend-str cutstart-str cutend-str)
  "Return time slice of a time range in minutes."
  (let ((tstart (org-time-string-to-seconds tstart-str))
    (tend (org-time-string-to-seconds tend-str))
    (cutstart (if (stringp cutstart-str)
              (org-time-string-to-seconds cutstart-str)
            cutstart-str))
    (cutend (if (stringp cutend-str)
            (org-time-string-to-seconds cutend-str)
          cutend-str))
    result)
    (setq result (max 0
              (/  (- (min tend cutend) (max tstart cutstart))
              60)))))

(defun dfeich/org-clock-hourly-report (struct tstart-str tend-str)
  "Return a structure containing a per hour report within an interval."
  (let* ((tstart (org-time-string-to-seconds tstart-str))
     (tend (org-time-string-to-seconds tend-str))
     (delta 3600)
     (intvls (cl-loop for tm from tstart to (- tend delta) by delta
              collect `(,tm ,(+ tm delta))))
     result)
    ;; iterate over the intervals for the final table
    (cl-loop for iv in intvls
         collect (list
              iv
              (let* ((cutstart (car iv))
                 (cutend (cadr iv))
                 (tmsum 0.0)
                 headings trlst)
            ;; iterate over the task structure
            (cl-loop
             for item in struct
             do (progn
                  (setq headings (car item)
                    trlst (cadr item)
                    ;; sum up the parts of the time
                    ;; ranges falling into this
                    ;; interval
                    tmsum (apply
                       #'+
                       (mapcar
                        (lambda (tr)
                          (dfeich/org-slice-tr (car tr)
                                   (cadr tr)
                                   cutstart
                                   cutend))
                        trlst))))
             if (> tmsum 0) collect `(,headings ,tmsum) into lst
             finally return lst))))))

(defun org-dblock-write:nagora-report (params)
 "Fill in a dynamic timesheet reporting block."
  (let* ((buffer (plist-get params :buffer))
     (day (symbol-name (plist-get params :day)))
     (tstart (if (string-match-p "^[0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}$" day)
             day
           (error "Error: day format must be in YYYY-mm-dd format")))
     (tend (concat day " 23:59"))
     (table (dfeich/org-clock-hourly-report
         (dfeich/org-clock-get-tr-for-ivl buffer tstart tend)
         tstart tend)))
    (insert (format "#+CAPTION: timesheet for day %s\n" day))
    (insert "|Time|Customer| Task |Minutes|\n|------\n")
    (cl-loop
     for item in table
     do (let ((ivl (car item))
          (entries (cadr item)))
      (cl-loop for e in entries
           do (let ((headings (car e))
                (minutes (cadr e)))
            (insert (concat
                 "|"
                 (format-time-string "%H:%M" (seconds-to-time
                                  (car ivl)))
                 "-"
                 (format-time-string "%H:%M" (seconds-to-time
                                  (cadr ivl)))
                 "|" (nth 1 headings)
                 "|" (car (last headings))
                 "|" (format "%d" minutes)
                 "|\n"))))))
    (insert "|----\n|TOTAL||||\n#+TBLFM: @>$>=vsum(@I..@II)")
    (search-backward "Time")
    (org-table-align)
    (org-table-recalculate '(16))))
Tried to clarify how to use a dynamic block. Unsure wheather users has experience with that.
Source Link
dfeich
  • 1.9k
  • 18
  • 17

My solution is by no means shorter than the one by Tobias, and I probably would not have written it, had I not been interested in one of the side products: A function which collects me all clock ranges with full heading information in a certain time interval, which is the basis for further processing. Based on this function and some other helpers, I implement a dynamic block which you can use like a normal org clocktable block. So, you just write the following, and upon hitting C-c C-c on the BEGIN line, it will expand to the table:


  #+BEGIN: nagora-report :buffer "nagora-example.org" :day 2016-02-05
  #+END:

  #+BEGIN: nagora-report :buffer "nagora-example.org" :day 2016-02-25
  #+END:

These two blocks will expand to the following:

I still wonder at this special timesheet reporting format, because it also is not translation invariant, so the same amount of work will look different, if you begin at the start of an hour or some time later. But sometimes it is shorter to implement such a function than discuss with management ;-)

(defun dfeich/org-clock-get-tr-for-ivl (buffer tstart-str tend-str &optional limit)
  "Return clocking information touching a given time interval."
  (cl-assert (and buffer (get-buffer buffer)) nil "Error: :buffer must be defined")
  (with-current-buffer buffer
    (save-excursion
      (let ((re (concat "^\\(\\*+[ \t]*.*\\)\\|^[ \t]*"
            org-clock-string
            "[ \t]*\\(?:\\(\\[.*?\\]\\)-+\\(\\[.*?\\]\\)\\|=>[ \t]+\\([0-9]+\\):\\([0-9]+\\)\\)"))
        (counter 0)
        (tmphd "BEFORE FIRST HEADING")
        (tstart (org-time-string-to-seconds tstart-str))
        (tend (org-time-string-to-seconds tend-str))
        (limit (or limit (point-max)))
        headings timelst
        lvl title result ts te)
    (goto-char (point-min))
    (cl-block myblock
      (while (re-search-forward re nil t)
        (cond
         ;; found a org heading
         ((match-end 1)
          (if (> (length timelst) 0)
          (setq result (nconc result (list (list
                            (copy-sequence headings)
                            timelst)))))
          (setq tmphd (org-heading-components)
            lvl (car tmphd)
            title (nth 4 tmphd)
            timelst nil)
          ;; maintain a list of the current heading hierarchy
          (cond
           ((> lvl (length headings))
        (setq headings  (nconc headings `(,title))))
           ((= lvl (length headings))
        (setf (nth (1- lvl) headings) title))
           ((< lvl (length headings))
        (setq headings (cl-subseq headings 0 lvl))
        (setf (nth (1- lvl) headings) title))))
         ;; found a clock line with 2 timestamps
         ((match-end 3)
          (setq ts (save-match-data (org-time-string-to-seconds
                     (match-string-no-properties 2)))
            te (save-match-data (org-time-string-to-seconds
                     (match-string-no-properties 3))))
          ;; the clock lines progress from newest to oldest. This
          ;; enables skipping the rest if this condition is true
          (if (> tstart te)
          (if (re-search-forward "^\\(\\*+[ \t]*.*\\)" nil t)
              (beginning-of-line)
            (goto-char (point-max)))
        (when (> tend ts)
          (setq timelst (nconc timelst (list
                        (list (match-string-no-properties 2)
                              (match-string-no-properties 3)))))))))
        (when (>= (point) limit)
          (cl-return-from myblock))))
    (if (> (length timelst) 0)
        (setq result (nconc result (list (list (copy-sequence headings)
                           timelst)))))
    result))))

(defun dfeich/org-slice-tr (tstart-str tend-str cutstart-str cutend-str)
  "Return time slice of a time range in minutes."
  (let ((tstart (org-time-string-to-seconds tstart-str))
    (tend (org-time-string-to-seconds tend-str))
    (cutstart (if (stringp cutstart-str)
              (org-time-string-to-seconds cutstart-str)
            cutstart-str))
    (cutend (if (stringp cutend-str)
            (org-time-string-to-seconds cutend-str)
          cutend-str))
    result)
    (setq result (max 0
              (/  (- (min tend cutend) (max tstart cutstart))
              60)))))

(defun dfeich/org-clock-hourly-report (struct tstart-str tend-str)
  "Return a structure containing a per hour report within an interval."
  (let* ((tstart (org-time-string-to-seconds tstart-str))
     (tend (org-time-string-to-seconds tend-str))
     (delta 3600)
     (intvls (cl-loop for tm from tstart to (- tend delta) by delta
              collect `(,tm ,(+ tm delta))))
     result)
    ;; iterate over the intervals for the final table
    (cl-loop for iv in intvls
         collect (list
              iv
              (let* ((cutstart (car iv))
                 (cutend (cadr iv))
                 (tmsum 0.0)
                 headings trlst)
            ;; iterate over the task structure
            (cl-loop
             for item in struct
             do (progn
                  (setq headings (car item)
                    trlst (cadr item)
                    ;; sum up the parts of the time
                    ;; ranges falling into this
                    ;; interval
                    tmsum (apply
                       #'+
                       (mapcar
                        (lambda (tr)
                          (dfeich/org-slice-tr (car tr)
                                   (cadr tr)
                                   cutstart
                                   cutend))
                        trlst))))
             if (> tmsum 0) collect `(,headings ,tmsum) into lst
             finally return lst))))))

(defun org-dblock-write:nagora-report (params)
 "Fill in a dynamic timesheet reporting block."
  (let* ((buffer (plist-get params :buffer))
     (day (symbol-name (plist-get params :day)))
     (tstart (if (string-match-p "^[0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}$" day)
             day
           (error "Error: day format must be in YYYY-mm-dd format")))
     (tend (concat day " 23:59"))
     (table (dfeich/org-clock-hourly-report
         (dfeich/org-clock-get-tr-for-ivl buffer tstart tend)
         tstart tend)))
    (insert (format "#+CAPTION: timesheet for day %s\n" day))
    (insert "|Time|Customer| Task |Minutes|\n|------\n")
    (cl-loop
     for item in table
     do (let ((ivl (car item))
          (entries (cadr item)))
      (cl-loop for e in entries
           do (let ((headings (car e))
                (minutes (cadr e)))
            (insert (concat
                 "|"
                 (format-time-string "%H:%M" (seconds-to-time
                                  (car ivl)))
                 "-"
                 (format-time-string "%H:%M" (seconds-to-time
                                  (cadr ivl)))
                 "|" (nth 1 headings)
                 "|" (car (last headings))
                 "|" (format "%d" minutes)
                 "|\n"))))))
    (insert "|----\n|TOTAL||||\n#+TBLFM: @>$>=vsum(@I..@II)")
    (search-backward "Time")
    (org-table-align)
    (org-table-recalculate '(16))))

My solution is by no means shorter than the one by Tobias, and I probably would not have written it, had I not been interested in one of the side products: A function which collects me all clock ranges with full heading information in a certain time interval, which is the basis for further processing. Based on this function and some other helpers, I implement a dynamic block which you can use like a normal org clocktable block:

I still wonder at this special format, because it also is not translation invariant, so the same amount of work will look different, if you begin at the start of an hour or some time later. But sometimes it is shorter to implement such a function than discuss with management ;-)

(defun dfeich/org-clock-get-tr-for-ivl (buffer tstart-str tend-str &optional limit)
  "Return clocking information touching a given time interval."
  (cl-assert (and buffer (get-buffer buffer)) nil "Error: :buffer must be defined")
  (with-current-buffer buffer
    (save-excursion
      (let ((re (concat "^\\(\\*+[ \t]*.*\\)\\|^[ \t]*"
            org-clock-string
            "[ \t]*\\(?:\\(\\[.*?\\]\\)-+\\(\\[.*?\\]\\)\\|=>[ \t]+\\([0-9]+\\):\\([0-9]+\\)\\)"))
        (counter 0)
        (tmphd "BEFORE FIRST HEADING")
        (tstart (org-time-string-to-seconds tstart-str))
        (tend (org-time-string-to-seconds tend-str))
        (limit (or limit (point-max)))
        headings timelst
        lvl title result ts te)
    (goto-char (point-min))
    (cl-block myblock
      (while (re-search-forward re nil t)
        (cond
         ;; found a org heading
         ((match-end 1)
          (if (> (length timelst) 0)
          (setq result (nconc result (list (list
                            (copy-sequence headings)
                            timelst)))))
          (setq tmphd (org-heading-components)
            lvl (car tmphd)
            title (nth 4 tmphd)
            timelst nil)
          ;; maintain a list of the current heading hierarchy
          (cond
           ((> lvl (length headings))
        (setq headings  (nconc headings `(,title))))
           ((= lvl (length headings))
        (setf (nth (1- lvl) headings) title))
           ((< lvl (length headings))
        (setq headings (cl-subseq headings 0 lvl))
        (setf (nth (1- lvl) headings) title))))
         ;; found a clock line with 2 timestamps
         ((match-end 3)
          (setq ts (save-match-data (org-time-string-to-seconds
                     (match-string-no-properties 2)))
            te (save-match-data (org-time-string-to-seconds
                     (match-string-no-properties 3))))
          ;; the clock lines progress from newest to oldest. This
          ;; enables skipping the rest if this condition is true
          (if (> tstart te)
          (if (re-search-forward "^\\(\\*+[ \t]*.*\\)" nil t)
              (beginning-of-line)
            (goto-char (point-max)))
        (when (> tend ts)
          (setq timelst (nconc timelst (list
                        (list (match-string-no-properties 2)
                              (match-string-no-properties 3)))))))))
        (when (>= (point) limit)
          (cl-return-from myblock))))
    (if (> (length timelst) 0)
        (setq result (nconc result (list (list (copy-sequence headings)
                           timelst)))))
    result))))

(defun dfeich/org-slice-tr (tstart-str tend-str cutstart-str cutend-str)
  "Return time slice of a time range in minutes."
  (let ((tstart (org-time-string-to-seconds tstart-str))
    (tend (org-time-string-to-seconds tend-str))
    (cutstart (if (stringp cutstart-str)
              (org-time-string-to-seconds cutstart-str)
            cutstart-str))
    (cutend (if (stringp cutend-str)
            (org-time-string-to-seconds cutend-str)
          cutend-str))
    result)
    (setq result (max 0
              (/  (- (min tend cutend) (max tstart cutstart))
              60)))))

(defun dfeich/org-clock-hourly-report (struct tstart-str tend-str)
  "Return a structure containing a per hour report within an interval."
  (let* ((tstart (org-time-string-to-seconds tstart-str))
     (tend (org-time-string-to-seconds tend-str))
     (delta 3600)
     (intvls (cl-loop for tm from tstart to (- tend delta) by delta
              collect `(,tm ,(+ tm delta))))
     result)
    ;; iterate over the intervals for the final table
    (cl-loop for iv in intvls
         collect (list
              iv
              (let* ((cutstart (car iv))
                 (cutend (cadr iv))
                 (tmsum 0.0)
                 headings trlst)
            ;; iterate over the task structure
            (cl-loop
             for item in struct
             do (progn
                  (setq headings (car item)
                    trlst (cadr item)
                    ;; sum up the parts of the time
                    ;; ranges falling into this
                    ;; interval
                    tmsum (apply
                       #'+
                       (mapcar
                        (lambda (tr)
                          (dfeich/org-slice-tr (car tr)
                                   (cadr tr)
                                   cutstart
                                   cutend))
                        trlst))))
             if (> tmsum 0) collect `(,headings ,tmsum) into lst
             finally return lst))))))

(defun org-dblock-write:nagora-report (params)
  (let* ((buffer (plist-get params :buffer))
     (day (symbol-name (plist-get params :day)))
     (tstart (if (string-match-p "^[0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}$" day)
             day
           (error "Error: day format must be in YYYY-mm-dd format")))
     (tend (concat day " 23:59"))
     (table (dfeich/org-clock-hourly-report
         (dfeich/org-clock-get-tr-for-ivl buffer tstart tend)
         tstart tend)))
    (insert (format "#+CAPTION: timesheet for day %s\n" day))
    (insert "|Time|Customer| Task |Minutes|\n|------\n")
    (cl-loop
     for item in table
     do (let ((ivl (car item))
          (entries (cadr item)))
      (cl-loop for e in entries
           do (let ((headings (car e))
                (minutes (cadr e)))
            (insert (concat
                 "|"
                 (format-time-string "%H:%M" (seconds-to-time
                                  (car ivl)))
                 "-"
                 (format-time-string "%H:%M" (seconds-to-time
                                  (cadr ivl)))
                 "|" (nth 1 headings)
                 "|" (car (last headings))
                 "|" (format "%d" minutes)
                 "|\n"))))))
    (insert "|----\n|TOTAL||||\n#+TBLFM: @>$>=vsum(@I..@II)")
    (search-backward "Time")
    (org-table-align)
    (org-table-recalculate '(16))))

My solution is by no means shorter than the one by Tobias, and I probably would not have written it, had I not been interested in one of the side products: A function which collects me all clock ranges with full heading information in a certain time interval, which is the basis for further processing. Based on this function and some other helpers, I implement a dynamic block which you can use like a normal org clocktable block. So, you just write the following, and upon hitting C-c C-c on the BEGIN line, it will expand to the table:


  #+BEGIN: nagora-report :buffer "nagora-example.org" :day 2016-02-05
  #+END:

  #+BEGIN: nagora-report :buffer "nagora-example.org" :day 2016-02-25
  #+END:

These two blocks will expand to the following:

I still wonder at this special timesheet reporting format, because it also is not translation invariant, so the same amount of work will look different, if you begin at the start of an hour or some time later. But sometimes it is shorter to implement such a function than discuss with management ;-)

(defun dfeich/org-clock-get-tr-for-ivl (buffer tstart-str tend-str &optional limit)
  "Return clocking information touching a given time interval."
  (cl-assert (and buffer (get-buffer buffer)) nil "Error: :buffer must be defined")
  (with-current-buffer buffer
    (save-excursion
      (let ((re (concat "^\\(\\*+[ \t]*.*\\)\\|^[ \t]*"
            org-clock-string
            "[ \t]*\\(?:\\(\\[.*?\\]\\)-+\\(\\[.*?\\]\\)\\|=>[ \t]+\\([0-9]+\\):\\([0-9]+\\)\\)"))
        (counter 0)
        (tmphd "BEFORE FIRST HEADING")
        (tstart (org-time-string-to-seconds tstart-str))
        (tend (org-time-string-to-seconds tend-str))
        (limit (or limit (point-max)))
        headings timelst
        lvl title result ts te)
    (goto-char (point-min))
    (cl-block myblock
      (while (re-search-forward re nil t)
        (cond
         ;; found a org heading
         ((match-end 1)
          (if (> (length timelst) 0)
          (setq result (nconc result (list (list
                            (copy-sequence headings)
                            timelst)))))
          (setq tmphd (org-heading-components)
            lvl (car tmphd)
            title (nth 4 tmphd)
            timelst nil)
          ;; maintain a list of the current heading hierarchy
          (cond
           ((> lvl (length headings))
        (setq headings  (nconc headings `(,title))))
           ((= lvl (length headings))
        (setf (nth (1- lvl) headings) title))
           ((< lvl (length headings))
        (setq headings (cl-subseq headings 0 lvl))
        (setf (nth (1- lvl) headings) title))))
         ;; found a clock line with 2 timestamps
         ((match-end 3)
          (setq ts (save-match-data (org-time-string-to-seconds
                     (match-string-no-properties 2)))
            te (save-match-data (org-time-string-to-seconds
                     (match-string-no-properties 3))))
          ;; the clock lines progress from newest to oldest. This
          ;; enables skipping the rest if this condition is true
          (if (> tstart te)
          (if (re-search-forward "^\\(\\*+[ \t]*.*\\)" nil t)
              (beginning-of-line)
            (goto-char (point-max)))
        (when (> tend ts)
          (setq timelst (nconc timelst (list
                        (list (match-string-no-properties 2)
                              (match-string-no-properties 3)))))))))
        (when (>= (point) limit)
          (cl-return-from myblock))))
    (if (> (length timelst) 0)
        (setq result (nconc result (list (list (copy-sequence headings)
                           timelst)))))
    result))))

(defun dfeich/org-slice-tr (tstart-str tend-str cutstart-str cutend-str)
  "Return time slice of a time range in minutes."
  (let ((tstart (org-time-string-to-seconds tstart-str))
    (tend (org-time-string-to-seconds tend-str))
    (cutstart (if (stringp cutstart-str)
              (org-time-string-to-seconds cutstart-str)
            cutstart-str))
    (cutend (if (stringp cutend-str)
            (org-time-string-to-seconds cutend-str)
          cutend-str))
    result)
    (setq result (max 0
              (/  (- (min tend cutend) (max tstart cutstart))
              60)))))

(defun dfeich/org-clock-hourly-report (struct tstart-str tend-str)
  "Return a structure containing a per hour report within an interval."
  (let* ((tstart (org-time-string-to-seconds tstart-str))
     (tend (org-time-string-to-seconds tend-str))
     (delta 3600)
     (intvls (cl-loop for tm from tstart to (- tend delta) by delta
              collect `(,tm ,(+ tm delta))))
     result)
    ;; iterate over the intervals for the final table
    (cl-loop for iv in intvls
         collect (list
              iv
              (let* ((cutstart (car iv))
                 (cutend (cadr iv))
                 (tmsum 0.0)
                 headings trlst)
            ;; iterate over the task structure
            (cl-loop
             for item in struct
             do (progn
                  (setq headings (car item)
                    trlst (cadr item)
                    ;; sum up the parts of the time
                    ;; ranges falling into this
                    ;; interval
                    tmsum (apply
                       #'+
                       (mapcar
                        (lambda (tr)
                          (dfeich/org-slice-tr (car tr)
                                   (cadr tr)
                                   cutstart
                                   cutend))
                        trlst))))
             if (> tmsum 0) collect `(,headings ,tmsum) into lst
             finally return lst))))))

(defun org-dblock-write:nagora-report (params)
 "Fill in a dynamic timesheet reporting block."
  (let* ((buffer (plist-get params :buffer))
     (day (symbol-name (plist-get params :day)))
     (tstart (if (string-match-p "^[0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}$" day)
             day
           (error "Error: day format must be in YYYY-mm-dd format")))
     (tend (concat day " 23:59"))
     (table (dfeich/org-clock-hourly-report
         (dfeich/org-clock-get-tr-for-ivl buffer tstart tend)
         tstart tend)))
    (insert (format "#+CAPTION: timesheet for day %s\n" day))
    (insert "|Time|Customer| Task |Minutes|\n|------\n")
    (cl-loop
     for item in table
     do (let ((ivl (car item))
          (entries (cadr item)))
      (cl-loop for e in entries
           do (let ((headings (car e))
                (minutes (cadr e)))
            (insert (concat
                 "|"
                 (format-time-string "%H:%M" (seconds-to-time
                                  (car ivl)))
                 "-"
                 (format-time-string "%H:%M" (seconds-to-time
                                  (cadr ivl)))
                 "|" (nth 1 headings)
                 "|" (car (last headings))
                 "|" (format "%d" minutes)
                 "|\n"))))))
    (insert "|----\n|TOTAL||||\n#+TBLFM: @>$>=vsum(@I..@II)")
    (search-backward "Time")
    (org-table-align)
    (org-table-recalculate '(16))))
Source Link
dfeich
  • 1.9k
  • 18
  • 17

My solution is by no means shorter than the one by Tobias, and I probably would not have written it, had I not been interested in one of the side products: A function which collects me all clock ranges with full heading information in a certain time interval, which is the basis for further processing. Based on this function and some other helpers, I implement a dynamic block which you can use like a normal org clocktable block:

  #+BEGIN: nagora-report :buffer "nagora-example.org" :day 2016-02-05
  #+CAPTION: timesheet for day 2016-02-05
  |        Time | Customer | Task       | Minutes |
  |-------------+----------+------------+---------|
  | 10:00-11:00 | PU [2/2] | PSMD Block |      17 |
  | 11:00-12:00 | PU [2/2] | PSMD Block |      49 |
  | 12:00-13:00 | PU [2/2] | PSMD Block |       2 |
  | 14:00-15:00 | PU [2/2] | PSMD Block |      33 |
  | 15:00-16:00 | PU [2/2] | PSMD Block |      60 |
  | 16:00-17:00 | PU [2/2] | PSMD Block |      36 |
  |-------------+----------+------------+---------|
  |       TOTAL |          |            |     197 |
  #+TBLFM: @>$>=vsum(@I..@II)
  #+END:
  
  #+BEGIN: nagora-report :buffer "nagora-example.org" :day 2016-02-25
  #+CAPTION: timesheet for day 2016-02-25
  |        Time | Customer | Task                          | Minutes |
  |-------------+----------+-------------------------------+---------|
  | 11:00-12:00 | PU [2/2] | UAT PSMD Roamers problem      |      14 |
  | 16:00-17:00 | PU [2/2] | STARTED Check TII code on UAT |       1 |
  | 16:00-17:00 | PU [2/2] | Update Coursework M26 branch  |       3 |
  | 17:00-18:00 | PU [2/2] | STARTED Check TII code on UAT |      60 |
  | 18:00-19:00 | PU [2/2] | STARTED Check TII code on UAT |      60 |
  | 19:00-20:00 | PU [2/2] | STARTED Check TII code on UAT |      60 |
  | 20:00-21:00 | PU [2/2] | STARTED Check TII code on UAT |      60 |
  | 21:00-22:00 | PU [2/2] | STARTED Check TII code on UAT |      60 |
  | 22:00-23:00 | PU [2/2] | STARTED Check TII code on UAT |      60 |
  |-------------+----------+-------------------------------+---------|
  |       TOTAL |          |                               |     378 |
  #+TBLFM: @>$>=vsum(@I..@II)
  #+END:

The counters in brackets can be easily cleaned up if desired. Currently the dynamic block wants an open buffer (:buffer argument) but it is trivial to change that to a file path.

I still wonder at this special format, because it also is not translation invariant, so the same amount of work will look different, if you begin at the start of an hour or some time later. But sometimes it is shorter to implement such a function than discuss with management ;-)

I hope this is useful. It certainly can be cleaned up a bit and made more consistent.

(defun dfeich/org-clock-get-tr-for-ivl (buffer tstart-str tend-str &optional limit)
  "Return clocking information touching a given time interval."
  (cl-assert (and buffer (get-buffer buffer)) nil "Error: :buffer must be defined")
  (with-current-buffer buffer
    (save-excursion
      (let ((re (concat "^\\(\\*+[ \t]*.*\\)\\|^[ \t]*"
            org-clock-string
            "[ \t]*\\(?:\\(\\[.*?\\]\\)-+\\(\\[.*?\\]\\)\\|=>[ \t]+\\([0-9]+\\):\\([0-9]+\\)\\)"))
        (counter 0)
        (tmphd "BEFORE FIRST HEADING")
        (tstart (org-time-string-to-seconds tstart-str))
        (tend (org-time-string-to-seconds tend-str))
        (limit (or limit (point-max)))
        headings timelst
        lvl title result ts te)
    (goto-char (point-min))
    (cl-block myblock
      (while (re-search-forward re nil t)
        (cond
         ;; found a org heading
         ((match-end 1)
          (if (> (length timelst) 0)
          (setq result (nconc result (list (list
                            (copy-sequence headings)
                            timelst)))))
          (setq tmphd (org-heading-components)
            lvl (car tmphd)
            title (nth 4 tmphd)
            timelst nil)
          ;; maintain a list of the current heading hierarchy
          (cond
           ((> lvl (length headings))
        (setq headings  (nconc headings `(,title))))
           ((= lvl (length headings))
        (setf (nth (1- lvl) headings) title))
           ((< lvl (length headings))
        (setq headings (cl-subseq headings 0 lvl))
        (setf (nth (1- lvl) headings) title))))
         ;; found a clock line with 2 timestamps
         ((match-end 3)
          (setq ts (save-match-data (org-time-string-to-seconds
                     (match-string-no-properties 2)))
            te (save-match-data (org-time-string-to-seconds
                     (match-string-no-properties 3))))
          ;; the clock lines progress from newest to oldest. This
          ;; enables skipping the rest if this condition is true
          (if (> tstart te)
          (if (re-search-forward "^\\(\\*+[ \t]*.*\\)" nil t)
              (beginning-of-line)
            (goto-char (point-max)))
        (when (> tend ts)
          (setq timelst (nconc timelst (list
                        (list (match-string-no-properties 2)
                              (match-string-no-properties 3)))))))))
        (when (>= (point) limit)
          (cl-return-from myblock))))
    (if (> (length timelst) 0)
        (setq result (nconc result (list (list (copy-sequence headings)
                           timelst)))))
    result))))

(defun dfeich/org-slice-tr (tstart-str tend-str cutstart-str cutend-str)
  "Return time slice of a time range in minutes."
  (let ((tstart (org-time-string-to-seconds tstart-str))
    (tend (org-time-string-to-seconds tend-str))
    (cutstart (if (stringp cutstart-str)
              (org-time-string-to-seconds cutstart-str)
            cutstart-str))
    (cutend (if (stringp cutend-str)
            (org-time-string-to-seconds cutend-str)
          cutend-str))
    result)
    (setq result (max 0
              (/  (- (min tend cutend) (max tstart cutstart))
              60)))))

(defun dfeich/org-clock-hourly-report (struct tstart-str tend-str)
  "Return a structure containing a per hour report within an interval."
  (let* ((tstart (org-time-string-to-seconds tstart-str))
     (tend (org-time-string-to-seconds tend-str))
     (delta 3600)
     (intvls (cl-loop for tm from tstart to (- tend delta) by delta
              collect `(,tm ,(+ tm delta))))
     result)
    ;; iterate over the intervals for the final table
    (cl-loop for iv in intvls
         collect (list
              iv
              (let* ((cutstart (car iv))
                 (cutend (cadr iv))
                 (tmsum 0.0)
                 headings trlst)
            ;; iterate over the task structure
            (cl-loop
             for item in struct
             do (progn
                  (setq headings (car item)
                    trlst (cadr item)
                    ;; sum up the parts of the time
                    ;; ranges falling into this
                    ;; interval
                    tmsum (apply
                       #'+
                       (mapcar
                        (lambda (tr)
                          (dfeich/org-slice-tr (car tr)
                                   (cadr tr)
                                   cutstart
                                   cutend))
                        trlst))))
             if (> tmsum 0) collect `(,headings ,tmsum) into lst
             finally return lst))))))

(defun org-dblock-write:nagora-report (params)
  (let* ((buffer (plist-get params :buffer))
     (day (symbol-name (plist-get params :day)))
     (tstart (if (string-match-p "^[0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}$" day)
             day
           (error "Error: day format must be in YYYY-mm-dd format")))
     (tend (concat day " 23:59"))
     (table (dfeich/org-clock-hourly-report
         (dfeich/org-clock-get-tr-for-ivl buffer tstart tend)
         tstart tend)))
    (insert (format "#+CAPTION: timesheet for day %s\n" day))
    (insert "|Time|Customer| Task |Minutes|\n|------\n")
    (cl-loop
     for item in table
     do (let ((ivl (car item))
          (entries (cadr item)))
      (cl-loop for e in entries
           do (let ((headings (car e))
                (minutes (cadr e)))
            (insert (concat
                 "|"
                 (format-time-string "%H:%M" (seconds-to-time
                                  (car ivl)))
                 "-"
                 (format-time-string "%H:%M" (seconds-to-time
                                  (cadr ivl)))
                 "|" (nth 1 headings)
                 "|" (car (last headings))
                 "|" (format "%d" minutes)
                 "|\n"))))))
    (insert "|----\n|TOTAL||||\n#+TBLFM: @>$>=vsum(@I..@II)")
    (search-backward "Time")
    (org-table-align)
    (org-table-recalculate '(16))))

Note: The solution is inspired by the org-clock-sum code and analogous to that function I chose not to invoke the full org parsers, but to use a probably more efficient direct parsing, since I'm only interested in headlines and clock lines.

The regexp search function uses an interesting concept copied from org-clock-sum involving an "or"-ed regexp which either matches a headline or a clock time range. That idea is kind of nice, since it allows one to implement a simple state machine with a single regexp in this way.