Offer Template
Offer Subtitle

;; alist of team members names and rates
(setq hourly-rates
      '((profile_1  . 10.00)
        (profile_2 . 20.00)))

;; you can explicitly set the overhead and the profit here, as a
;; percentage on top of the total amount computed for the task.
(setq overhead 0.20)
(setq profit   0.0)

;; if we are estimating without explicitly allocating people, budget
;; estimations will be computed using an average rage; the data below
;; is used to compute the average rate, weighted on the ballpark effort
;; allocations set below
(setq ballpark-effort-allocation
      '((profile_1  . 0.30)
        (profile_2 . 0.70)))

;; Functions to access association lists

(defun avm/keys   (alist) (mapcar (lambda (x) (car x)) alist))
(defun avm/values (alist) (mapcar (lambda (x) (cdr x)) alist))
(defun avm/value  (key alist) (cdr (assoc key alist)))

;; Computation of average and rate with overhead and profit

(defun avm/average-rate (hourly-rates effort-allocation)
  "Compute the average rate, given the specified effort allocation: SUM(PROD(RATE_i, EFFORT_i))

   - hourly-rates is an alist '((resource-name . rate) ...)
   - effort-allocation is an alist '((resource-name . effort) ...)"
  (apply '+ (mapcar (lambda (x) (* (avm/value x hourly-rates) (avm/value x ballpark-effort-allocation) ))
                    (avm/keys hourly-rates))))

(defun avm/effort-cost (effort-in-hours hourly-rate)
  "Compute COST = Effort * Rate"
  (* effort-in-hours hourly-rate))

;; Average and Compound hourly-rates                     
;; - Average rate is computed based on ballpark estimation of involvement
;;   of Adolfo and Michele
;; - Compound Rate includes overhead and profit on top of the
;;   ballpark estimation (with overhead and profit given as decimal points (.20 for 20%)

(setq average-rate (round (avm/average-rate hourly-rates ballpark-effort-allocation)))
(setq compound-rate (* (+ 1.00 overhead profit) average-rate))

;; Access org elements

(defun avm/org-get-property (key)
  "Get the association list of property 'key' of current entry.

     Notice that org-mode has all entries stored in upcase."
  (let ( (props (org-entry-properties)) )
    (assoc (upcase key) props)))

(defun avm/org-has-property (key)
  "Return t if the current entry has property with key 'key'"
  (not (equal nil (avm/org-get-property key))))

(defun avm/org-get-property-value (key)
  "Get the value of property 'key' in current entry as a string"
  (cdr (avm/org-get-property key)))

(defun avm/org-get-property-as-working-hours (property)
  "Get the value of property EFFORT in current entry"
  (avm/org-duration-string-to-working-hours (avm/org-get-property-value property)))

;; Transforming Org Mode durations to working hours (8 working hours / day)
(defvar working-hours-per-effort-day 8)

(defun avm/org-duration-string-to-working-hours (duration-string)
  (let* ( (minutes (round (org-duration-string-to-minutes duration-string)))
          (full-days (/ minutes 1440))   ; there are, in fact, 1440 minutes per day (* 24 60)
          (remaining (% minutes 1440)) )
    (+ (* full-days working-hours-per-effort-day) (/ remaining 60.0))))

;; We now want to support two types of effort specifications
;; 1. UNPROFILED: we have a property EFFORT and we compute the values
;; using the average hourly-rates
;; 2. PROFILED: we have a property similar to EFFORT_xxx where xxx
;; identifies a resource name in hourly-rates.  In this case we use
;; the actual hourly-rates with the specific efforts.
;; In case both are present, we use the generic entry (:EFFORT:)

(defun avm/profiled-property-name (key &optional property)
  "Return the name of a property 'property' profiled with 'key', e.g., EFFORT_profile_name.
Property defaults to EFFORT."
  (concat (or property "EFFORT") "_" (symbol-name key)))

(defun avm/org-get-profiled-property-value (key &optional property)
  "Get the value of a profiled property, e.g., the value associate to EFFORT_profile_name.
Property defaults to EFFORT."
  (avm/org-get-property-value (avm/profiled-property-name key property)))

(defun avm/org-get-profiled-effort-as-working-hours (key)
  "Get the value of a profiled effort as working hours, e.g., the value associate to EFFORT_profile_name"
  (avm/org-duration-string-to-working-hours (avm/org-get-profiled-property-value key "EFFORT")))

(defun avm/org-has-profiled-property (key &optional property)
  "Check if the entry has a profiled entry, e.g., EFFORT_adolfo.
Property defaults to EFFORT."
  (avm/org-has-property (avm/profiled-property-name key property)))

(defun avm/org-has-any-profiled-property (keys &optional property)
  "Check whether the entry has a property suffixed with any one of the elements in keys.

   - keys is a list of strings
   - property is a string

   Thus (avm/org-has-any-profiled-property '(\"adolfo\" \"michele\") \"EFFORT\") will return
   true if there is a profiled EFFORT (that is, either EFFORT_adolfo or EFFORT_michele)."
  (member t (mapcar (lambda (x) (avm/org-has-profiled-property x property)) keys)) )

;; Compute profiled effort costs and define functions
;; working on lists

(defun avm/profiled-effort-to-cost (key hourly-rates)
  "Compute the cost associated to the profiled effort 'key' in an entry.
Given Effort_michele XX, the function looks up Michele's rate in hourly-rates and multiplies by XX."
   (avm/value key hourly-rates)
   (if (avm/org-has-profiled-property key "EFFORT") (avm/org-get-profiled-effort-as-working-hours key) 0)))

(defun avm/profiled-efforts-to-costs (hourly-rates)
  "Compute the cost associated to profiled effort of all keys in hourly-rates. 
Return an alist '((resource . cost) ...)"
  (mapcar (lambda (x) (cons x (avm/profiled-effort-to-cost x hourly-rates)))
          (avm/keys hourly-rates)))

(defun avm/profiled-efforts-to-working-hours (hourly-rates)
  "Compute the effort in working hours associated to all keys in hourly-rates. 
Return an alist '((resource . working-hours) ...)"
  (mapcar (lambda (key) (cons key (if (avm/org-has-profiled-property key "EFFORT") (avm/org-get-profiled-effort-as-working-hours key) 0)))
          (avm/keys hourly-rates)))

   ;;; Here we do all the work: 
   ;;; - we iterate over all entries under the heading with ID plan and
   ;;; - we add costs, overhead, profit, and total, based on hourly-rates and
   ;;;   data in the heading

 (lambda () 
   (cond ((avm/org-has-property "EFFORT")
          (let* ( (rate average-rate)
                  (cost (avm/effort-cost (avm/org-get-property-as-working-hours "EFFORT") rate))
                  (oh-and-p  (* cost (+ overhead profit)))
                  (total (+ cost oh-and-p)) )
            (org-set-property "Effort-Working-Hours" (format "%.2f" (avm/org-get-property-as-working-hours "EFFORT") ))
            (org-set-property "Rate" (format "%.2f" rate ))
            (org-set-property "Cost"  (format "%.2f" cost))
            (org-set-property "Overhead-and-Profit" (format "%.2f" oh-and-p))
            (org-set-property "Total" (format "%.2f" total))

         ((avm/org-has-any-profiled-property (avm/keys hourly-rates) "EFFORT")
          (let* ( (cost-alist (avm/profiled-efforts-to-costs hourly-rates))
                  (working-hours-alist (avm/profiled-efforts-to-working-hours hourly-rates))
                  (cost (apply '+ (mapcar 'cdr cost-alist)))
                  (total-working-hours (apply '+ (mapcar 'cdr working-hours-alist)))
                  (oh-and-p  (* cost (+ overhead profit)))
                  (total (+ cost oh-and-p)) )
            (mapcar (lambda (x) (org-set-property (avm/profiled-property-name (car x) "Effort-Working-Hours") (format "%.2f" (cdr x)))) working-hours-alist)
            (org-set-property "Effort-Working-Hours"  (format "%.2f" total-working-hours))
            (mapcar (lambda (x) (org-set-property (avm/profiled-property-name (car x) "Rate") (format "%.2f" (cdr x)))) hourly-rates)
            (mapcar (lambda (x) (org-set-property (avm/profiled-property-name (car x) "Cost") (format "%.2f" (cdr x)))) cost-alist)
            (org-set-property "Cost"  (format "%.2f" cost))
            (org-set-property "Overhead-and-Profit" (format "%.2f" oh-and-p))
            (org-set-property "Total" (format "%.2f" total))

1. Context

Information necessary to put the project in context: pre-existing environments and tools, constraints, opportunities.

2. Goals and Expected Results

  • What do we want to achieve?
  • What will we achieve in the project?

3. Functional Groups and Functions

All todo items here contribute to the project budget if they have an effort property set. Effort can either be generic (meaning any resource might work on the task; the average rate will be used) or specific, if using properties such as EFFORT_<resource_name>.


  • Use durations expressed in timing units (e.g., 4d, 10:10); plain numbers are interpreted as minutes
  • Use either profiled or plain efforts; if an entry contains both, the plain effort entry is used.

3.1. User Story 1

3.1.1. Task 1.1

In this task we will do such and such …

3.1.2. Task 1.2

In this other task we will do such and such …

3.1.3. Task 1.3

In this task we will do such and such …

4. Timing

This has to be filled by hand. In the future it might be interesting to try and integrate with a (Gantt) charting tool, such as Vega light or Task Juggler.

5. Budget

Budget rows have to be inserted by hand. VAT and Total are computed. In our experience the data computed from todos need some adjustments/grouping to make it into the budget.

You can use the previous (Budget by Item) or the last section (Budget Detailed Data) of this document to get summary data and put it in the budget.

Group 1 10500
Total (Before VAT) 10500.00
VAT 2310.00
Total (with VAT) 12810.00

6. Payments Structure

Set the first two columns of this table and the other columns will be computed for you.

Date Amount Net VAT Payment
01/03/2022 50% 5250.00 1155.00 6405.00
21/06/2022 10% 1050.00 231.00 1281.00
21/11/2022 40% 4200.00 924.00 5124.00

7. Additional Conditions

8. Risks Management

Risk Probability Impact Action Reference

9. Company/Professional Profile