Short and sweet
Make sure to keep a backup copy of your file just in case. Then add the following section at the bottom of the file:
* Code :noexport:
#+begin_src elisp :results drawer
(defun my/org-close-done-headline()
(let ((org-use-last-clock-out-time-as-effective-time t))
(org-todo "")
(org-todo "DONE")))
(org-map-entries #'my/org-close-done-headline "TODO=\"DONE\"" 'file)
#+end_src
Then place the cursor somewhere on the source code block and press C-c C-c
. That's it: magic in five lines of lisp code.
Long explanation
This kind of problem ("do something on every headline that meets some criteria") is tailor-made for the function org-map-entries
(see its doc string with C-h f org-map-entries
and the Mapping API section of the manual with C-h i g(emacs)Using the Mapping API
). It basically does the looping over headlines for you, it can optionally filter headlines based on some matching criteria, and it can apply a function on each headline it selects. And it can affect headlines in one or many files depending on its scope
argument.
In your case, we want to select headlines with a TODO
state equal to DONE
, in the scope of a single file and we want to do something on each one of these headlines. Let's imagine that we have a function do-something
that will do whatever it is that we want to do to each headline. Then all we have to do is
(org-map-entries #'do-something "TODO=\"DONE\"" 'file)
You can test this on your file by writing a simple do-something
function:
(defun do-something ()
(org-entry-get nil "ITEM"))
When you do M-: (do-something)
on a headline, it will give you the text of the headline. Now if you do M-: (org-map-entries #'do-something "TODO=\"DONE\"" 'file)
, it will give you a list of all the headlines that are marked DONE
in the file. You should try this.
A convenient way to do that is to use Org Babel to add some code snippets to the file you are going to be processing. Add a new section at the end (I usually call it "Code") and add a code block. Here's an example that you can cut and paste:
* DONE foo
* bar
* DONE baz
* Code :noexport:
#+begin_src elisp :results drawer
(defun do-something()
(org-entry-get nil "ITEM"))
(org-map-entries #'do-something "TODO=\"DONE\"" 'file)
#+end_src
#+RESULTS:
:results:
(foo baz)
:end:
Putting your cursor on the code block and pressing C-c C-c
evaluates the code block (i.e. it defines the do-something
function and then evaluates the org-map-entries
call to produce the results shown: the two headlines that are marked DONE
.
Now all we have to do is make our do-something
function do something more. In fact, let's give it a more descriptive name while we are at it. We'll call it my/org-close-done-headline
. We'll do that in three steps:
we'll figure out how to add a CLOSE:
but the time will be the current time.
then we'll figure out how to add a CLOSE:
but with a time that we specify.
then we'll figure out how to get the appropriate time out of the LOGBOOK
.
You can set the TODO state of a headline to e.g. DONE
, by doing M-: (org-todo "DONE")
while your cursor is on that headline (actually, from anywhere inside that section). The only trouble is that if it's already in DONE
state, nothing happens. So we trick it with two calls of org-todo
: the first call sets the TODO
state to empty, and the second resets it to DONE
but now since org-log-done
is set to time
, a CLOSE:
entry will get added, albeit with the current time:
* DONE foo
* bar
* DONE baz
* Code :noexport:
#+begin_src elisp :results drawer
(defun my/org-close-done-headline()
(org-todo "")
(org-todo "DONE"))
(org-map-entries #'my/org-close-done-headline "TODO=\"DONE\"" 'file)
#+end_src
Now if you C-c C-c
on the code block, the my/org-close-done
function will be applied on every DONE
headline, so it will reset it to empty and back to DONE
, adding the CLOSE:
entry in the process. Try it and see.
The second and third steps are actually one step and a very simple one at that, because Org mode conveniently provides a feature that lets org-todo
use the last clock-out time instead of the current time. It is controlled by the option org-use-last-clock-out-time-as-effective-time
whose doc string says:
When non-nil, use the last clock out time for ‘org-todo’.
Note that this option has precedence over the combined use of
‘org-use-effective-time’ and ‘org-extend-today-until’.
So all we have to do is modify the my/org-close-done-headline
just a bit:
....
#+begin_src elisp
(defun my/org-close-done-headline()
(let ((org-use-last-clock-out-time-as-effective-time t))
(org-todo "")
(org-todo "DONE")))
(org-map-entries #'my/org-close-done-headline "TODO=\"DONE\"" 'file)
#+end_src
We just let-bind the variable to t
for the duration of the two org-todo
calls and that's that: a fairly complicated transformation in five lines of lisp code.
One word of caution: just to be on the safe side, back up your file!
Although you can always use undo
if something goes wrong, having an extra copy of the file safely squirreled away will let you experiment without fear.