;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; balsheet-pnl.scm: multi-column report. includes ;; balance-sheet and p&l reports. ;; ;; By Christopher Lam, 2018 ;; ;; Improved from balance-sheet.scm ;; ;; This program is free software; you can redistribute it and/or ;; modify it under the terms of the GNU General Public License as ;; published by the Free Software Foundation; either version 2 of ;; the License, or (at your option) any later version. ;; ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with this program; if not, contact: ;; ;; Free Software Foundation Voice: +1-617-542-5942 ;; 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 ;; Boston, MA 02110-1301, USA gnu@gnu.org ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (define-module (gnucash report standard-reports balsheet-pnl)) (use-modules (gnucash utilities)) (use-modules (gnucash gnc-module)) (use-modules (gnucash gettext)) (use-modules (srfi srfi-1)) (gnc:module-load "gnucash/report/report-system" 0) (define FOOTER-TEXT "WARNING: Please be aware these figures are not guaranteed to be correct. Foreign currency conversions are not confirmed correct. Bug reports are very welcome at https://bugs.gnucash.org/ or please email the team at gnucash-devel@gnucash.org. No calculations for capital gains, or unrealized gains are made. Use the amounts at your own risk.") ;; define all option's names and help text so that they are properly (define optname-company-name (N_ "Company name")) (define opthelp-company-name (N_ "Name of company/individual.")) (define optname-startdate (N_ "Start Date")) (define optname-enddate (N_ "End Date")) (define optname-period (N_ "Period duration")) (define opthelp-period (N_ "Duration between time periods")) (define optname-dual-columns (N_ "Enable dual columns")) (define opthelp-dual-columns (N_ "Selecting this option will enable double-column \ reporting.")) (define optname-disable-amount-indent (N_ "Disable amount indenting")) (define opthelp-disable-amount-indent (N_ "Selecting this option will disable amount indenting, and condense amounts into a single column.")) (define optname-options-summary (N_ "Add options summary")) (define opthelp-options-summary (N_ "Add summary of options.")) (define optname-account-full-name (N_ "Account full name instead of indenting")) (define opthelp-account-full-name (N_ "Selecting this option enables full account name instead, and disables indenting account names.")) (define optname-accounts (N_ "Accounts")) (define opthelp-accounts (N_ "Report on these accounts, if display depth allows.")) (define optname-depth-limit (N_ "Levels of Subaccounts")) (define opthelp-depth-limit (N_ "Maximum number of levels in the account tree displayed.")) (define optname-parent-balance-mode (N_ "Parent account amounts include children")) (define opthelp-parent-balance-mode (N_ "If this option is enabled, subtotals are \ displayed within parent amounts, and if parent has own amount, it is displayed on \ the next row as a child account. If this option is disabled, subtotals are displayed \ below parent and children groups.")) (define optname-show-zb-accts (N_ "Include accounts with zero total balances")) (define opthelp-show-zb-accts (N_ "Include accounts with zero total (recursive) balances in this report.")) (define optname-omit-zb-bals (N_ "Omit zero balance figures")) (define opthelp-omit-zb-bals (N_ "Show blank space in place of any zero balances which would be shown.")) (define optname-account-links (N_ "Display accounts as hyperlinks")) (define opthelp-account-links (N_ "Shows each account in the table as a hyperlink to its register window.")) (define optname-amount-links (N_ "Display amounts as hyperlinks")) (define opthelp-amount-links (N_ "Shows each amounts in the table as a hyperlink to a register or report.")) ;; closing entries filter - for P&L report (define pagename-entries "Entries") (define optname-closing-pattern (N_ "Closing Entries pattern")) (define opthelp-closing-pattern (N_ "Any text in the Description column which identifies closing entries.")) (define optname-closing-casing (N_ "Closing Entries pattern is case-sensitive")) (define opthelp-closing-casing (N_ "Causes the Closing Entries Pattern match to be case-sensitive.")) (define optname-closing-regexp (N_ "Closing Entries Pattern is regular expression")) (define opthelp-closing-regexp (N_ "Causes the Closing Entries Pattern to be treated as a regular expression.")) ;; section labels (define optname-label-sections (N_ "Label sections")) (define opthelp-label-sections (N_ "Whether or not to include a label for sections.")) (define optname-total-sections (N_ "Include totals")) (define opthelp-total-sections (N_ "Whether or not to include a line indicating total amounts.")) ;; commodities (define pagename-commodities (N_ "Commodities")) (define optname-include-chart (N_ "Enable chart")) (define opthelp-include-chart (N_ "Enable link to barchart report")) (define optname-common-currency (N_ "Convert to common currency")) (define opthelp-common-currency (N_ "Convert all amounts to a single currency.")) (define optname-report-commodity (N_ "Report's currency")) (define optname-price-source (N_ "Price Source")) (define opthelp-price-source (N_ "How to determine exchange rates.")) (define optname-show-foreign (N_ "Show Foreign Currencies")) (define opthelp-show-foreign (N_ "Display any foreign currency amount in an account.")) (define optname-include-overall-period (N_ "If more than 1 period column, include overall period?")) (define opthelp-include-overall-period (N_ "If several profit & loss period columns are shown, \ also show overall period profit & loss.")) (define optname-show-rates (N_ "Show Exchange Rates")) (define opthelp-show-rates (N_ "Show the exchange rates used.")) (define trep-uuid "2fe3b9833af044abb929a88d5a59620f") (define networth-barchart-uuid "cbba1696c8c24744848062c7f1cf4a72") (define pnl-barchart-uuid "80769921e87943adade887b9835a7685") (define periodlist (list (cons #f (list (cons 'delta #f) (cons 'text (_ "disabled")) (cons 'tip (_ "disable multicolumn")))) (cons 'year (list (cons 'delta YearDelta) (cons 'text (_ "year")) (cons 'tip (_ "every year")))) (cons 'halfyear (list (cons 'delta HalfYearDelta) (cons 'text (_ "half-year")) (cons 'tip (_ "every half year")))) (cons 'quarter (list (cons 'delta QuarterDelta) (cons 'text (_ "quarter")) (cons 'tip (_ "every three months")))) (cons 'month (list (cons 'delta MonthDelta) (cons 'text (_ "month")) (cons 'tip (_ "every month")))) (cons 'twoweek (list (cons 'delta TwoWeekDelta) (cons 'text (_ "two weeks")) (cons 'tip (_ "every fortnight")))) (cons 'week (list (cons 'delta WeekDelta) (cons 'text (_ "week")) (cons 'tip (_ "every 7 days")))))) (define pricesource-list-common (list (cons 'pricedb-latest (list (cons 'text (_ "Most recent")) (cons 'tip (_ "The most recent recorded price.")))) (cons 'weighted-average (list (cons 'text (_ "Weighted Average")) (cons 'tip (_ "The weighted average of all currency transactions of the past.")))) (cons 'average-cost (list (cons 'text (_ "Average Cost")) (cons 'tip (_ "The volume-weighted average cost of purchases.")))))) (define pricesource-list-balsheet (reverse (cons (cons 'pricedb-nearest (list (cons 'text (_ "Nearest in time")) (cons 'tip (_ "The price recorded nearest in time to the column date.")))) pricesource-list-common))) (define pricesource-list-pnl (reverse (cons* (cons 'startperiod (list (cons 'text (_ "Nearest to start of period")) (cons 'tip (_ "Prices closest to the start of the reporting period \ are used.")))) (cons 'midperiod (list (cons 'text (_ "Nearest to mid of period")) (cons 'tip (_ "Prices in the middle of the reporting period \ are used.")))) (cons 'endperiod (list (cons 'text (_ "Nearest to end of period")) (cons 'tip (_ "Prices in the end of the reporting period \ are used.")))) pricesource-list-common))) (define (keylist->vectorlist keylist) (map (lambda (item) (vector (car item) (keylist-get-info keylist (car item) 'text) (keylist-get-info keylist (car item) 'tip))) keylist)) (define (keylist-get-info keylist key info) (cdr (assq info (cdr (assq key keylist))))) (define* (gnc:account-get-balances-at-dates account dates-list #:key ignore-closing?) (define (amount->monetary bal) (gnc:make-gnc-monetary (xaccAccountGetCommodity account) bal)) (let loop ((splits (xaccAccountGetSplitList account)) (dates-list dates-list) (currentbal 0) (lastbal 0) (balancelist '())) (cond ;; end of dates. job done! ((null? dates-list) (map amount->monetary (reverse balancelist))) ;; end of splits, but still has dates. pad with last-bal ;; until end of dates. ((null? splits) (loop '() (cdr dates-list) currentbal lastbal (cons lastbal balancelist))) (else (let* ((this (car splits)) (rest (cdr splits)) (currentbal (if (and ignore-closing? (xaccTransGetIsClosingTxn (xaccSplitGetParent this))) currentbal (+ (xaccSplitGetAmount this) currentbal))) (next (and (pair? rest) (car rest)))) (cond ;; the next split is still before date ((and next (< (xaccTransGetDate (xaccSplitGetParent next)) (car dates-list))) (loop rest dates-list currentbal lastbal balancelist)) ;; this split after date, add previous bal to balancelist ((< (car dates-list) (xaccTransGetDate (xaccSplitGetParent this))) (loop splits (cdr dates-list) lastbal lastbal (cons lastbal balancelist))) ;; this split before date, next split after date, or end. (else (loop rest (cdr dates-list) currentbal currentbal (cons currentbal balancelist))))))))) ;; options generator (define (multicol-report-options-generator report-type) (let* ((options (gnc:new-options)) (book (gnc-get-current-book)) (add-option (lambda (new-option) (gnc:register-option options new-option)))) (add-option (gnc:make-string-option gnc:pagename-general optname-company-name "b" opthelp-company-name (or (gnc:company-info book gnc:*company-name*) ""))) ;; date at which to report balance (gnc:options-add-date-interval! options gnc:pagename-general optname-startdate optname-enddate "c") (add-option (gnc:make-multichoice-callback-option gnc:pagename-general optname-period "c2" opthelp-period #f (keylist->vectorlist periodlist) #f (lambda (x) (gnc-option-db-set-option-selectable-by-name options gnc:pagename-general optname-disable-amount-indent (not x)) (gnc-option-db-set-option-selectable-by-name options gnc:pagename-general optname-dual-columns (not x)) (gnc-option-db-set-option-selectable-by-name options gnc:pagename-general (case report-type ((balsheet) optname-startdate) ((pnl) optname-include-overall-period)) x)))) (add-option (gnc:make-simple-boolean-option gnc:pagename-general optname-disable-amount-indent "c3" opthelp-disable-amount-indent #f)) (add-option (gnc:make-simple-boolean-option gnc:pagename-general optname-include-chart "d" opthelp-include-chart #f)) (add-option (gnc:make-simple-boolean-option gnc:pagename-general optname-dual-columns "c4" opthelp-dual-columns #t)) (add-option (gnc:make-multichoice-option gnc:pagename-general optname-options-summary "d" opthelp-options-summary 'never (list (vector 'always (_ "Always") (_ "Always display summary.")) (vector 'never (_ "Never") (_ "Disable report summary."))))) ;; accounts to work on (add-option (gnc:make-account-list-option gnc:pagename-accounts optname-accounts "a" opthelp-accounts (lambda () (gnc:filter-accountlist-type (list ACCT-TYPE-BANK ACCT-TYPE-CASH ACCT-TYPE-CREDIT ACCT-TYPE-ASSET ACCT-TYPE-LIABILITY ACCT-TYPE-STOCK ACCT-TYPE-MUTUAL ACCT-TYPE-CURRENCY ACCT-TYPE-PAYABLE ACCT-TYPE-RECEIVABLE ACCT-TYPE-EQUITY ACCT-TYPE-INCOME ACCT-TYPE-EXPENSE ACCT-TYPE-TRADING) (gnc-account-get-descendants-sorted (gnc-get-current-root-account)))) #f #t)) (gnc:options-add-account-levels! options gnc:pagename-accounts optname-depth-limit "b" opthelp-depth-limit 'all) ;; all about currencies (add-option (gnc:make-complex-boolean-option pagename-commodities optname-common-currency "b" opthelp-common-currency #f #f (lambda (x) (for-each (lambda (optname) (gnc-option-db-set-option-selectable-by-name options pagename-commodities optname x)) (list optname-report-commodity optname-show-rates optname-show-foreign optname-price-source))))) (gnc:options-add-currency! options pagename-commodities optname-report-commodity "c") (add-option (gnc:make-multichoice-option pagename-commodities optname-price-source "d" opthelp-price-source (case report-type ((pnl) 'midperiod) ((balsheet) 'pricedb-nearest)) (keylist->vectorlist (case report-type ((pnl) pricesource-list-pnl) ((balsheet) pricesource-list-balsheet))))) (add-option (gnc:make-simple-boolean-option pagename-commodities optname-show-foreign "e" opthelp-show-foreign #t)) (add-option (gnc:make-simple-boolean-option pagename-commodities optname-show-rates "f" opthelp-show-rates #t)) ;; what to show for zero-balance accounts (add-option (gnc:make-simple-boolean-option gnc:pagename-display optname-show-zb-accts "a" opthelp-show-zb-accts #t)) (add-option (gnc:make-simple-boolean-option gnc:pagename-display optname-omit-zb-bals "b" opthelp-omit-zb-bals #f)) (add-option (gnc:make-simple-boolean-option gnc:pagename-display optname-parent-balance-mode "c" opthelp-parent-balance-mode #t)) ;; some detailed formatting options (add-option (gnc:make-simple-boolean-option gnc:pagename-display optname-account-links "e" opthelp-account-links #t)) (add-option (gnc:make-simple-boolean-option gnc:pagename-display optname-amount-links "e5" opthelp-amount-links #t)) (add-option (gnc:make-simple-boolean-option gnc:pagename-display optname-account-full-name "f" opthelp-account-full-name #f)) (add-option (gnc:make-simple-boolean-option gnc:pagename-display optname-label-sections "g" opthelp-label-sections #t)) (add-option (gnc:make-simple-boolean-option gnc:pagename-display optname-total-sections "h" opthelp-total-sections #t)) (when (eq? report-type 'pnl) ;; include overall period column? (add-option (gnc:make-simple-boolean-option gnc:pagename-general optname-include-overall-period "e" opthelp-include-overall-period #f)) ;; closing entry match criteria (add-option (gnc:make-string-option pagename-entries optname-closing-pattern "a" opthelp-closing-pattern (_ "Closing Entries"))) (add-option (gnc:make-simple-boolean-option pagename-entries optname-closing-casing "b" opthelp-closing-casing #f)) (add-option (gnc:make-simple-boolean-option pagename-entries optname-closing-regexp "c" opthelp-closing-regexp #f))) ;; Set the accounts page as default option tab (gnc:options-set-default-section options gnc:pagename-accounts) options)) (define* (add-multicolumn-acct-table table title accountlist maxindent get-cell-monetary-fn cols-data #:key (omit-zb-bals? #f) (show-zb-accts? #t) (disable-account-indent? #f) (disable-amount-indent? #f) (show-orig-cur? #t) (show-title? #t) (show-accounts? #t) (show-total? #t) (depth-limit #f) (negate-amounts? #f) (recursive-bals? #f) (account-anchor? #t) (get-col-header-fn #f) (convert-curr-fn #f) (get-cell-anchor-fn #f)) ;; this function will add a 2D grid into the html-table ;; the data cells are generated from (get-cell-monetary-fn account col-datum) ;; the data cells may request an alternative (eg. original currency) monetary ;; horizontal labels are generated from calling (get-col-header-fn col-datum) ;; vertical labels are the account list. it can have multilevel subtotals. ;; the following are compulsory arguments: ;; table - an existing html-table object ;; title - string as the first row ;; accountlist - list of accounts ;; maxindent - maximum account depth ;; cols-data - list of data to be passed as parameter to the following helper functions ;; get-cell-monetary-fn - a lambda (account cols-data) which produces a gnc-monetary or #f (eg price conversion impossible) ;; the following are optional: ;; omit-zb-bals? - a boolean to omit "$0.00" amounts ;; show-zb-accts? - a boolean to omit whole account lines where all amounts are $0.00 (eg closed accts) ;; show-title? - a bool to show/hide individual sections: title row ;; show-accounts? - a bool to show/hide individual sections: accounts list and data columns ;; show-total? - a bool to show/hide individual sections: accounts total ;; disable-account-indent? - a boolean to disable narrow-cell indenting, and render account full-name instead ;; disable-amount-indent? - a bool to disable amount indenting (only for single data column reports) ;; negate-amounts? - a boolean to negate amounts. useful for e.g. income-type accounts. ;; depth-limit - (untested) accounts whose levels exceed this depth limit are not shown ;; recursive-bals? - a boolean to confirm recursive-balances enabled (parent-accounts show balances) or ;; disabled (multilevel subtotals after each parent+children) ;; account-anchor? - a boolean to enable/disable account link to account ;; amount-anchor? - a boolean to enable/disable amount link to report/register ;; get-col-header-fn - a lambda (accounts cols-data) to produce html-object - this is optional ;; convert-curr-fn - a lambda (monetary cols-data) which produces a gnc-monetary or #f - optional ;; show-orig-cur? - a boolean to enable/disable original currency after convert-curr-fn ;; get-cell-anchor-fn - a lambda (account cols-data) which produces a url string - optional (define num-columns (length cols-data)) (define amount-indenting? (and (not disable-amount-indent?) (= num-columns 1))) (define (make-list-thunk n thunk) (let loop ((result '()) (n n)) (if (zero? n) result (loop (cons (thunk) result) (1- n))))) (define (make-narrow-cell) (let ((narrow (gnc:make-html-table-cell/markup "text-cell" #f))) (gnc:html-table-cell-set-style! narrow "text-cell" 'attribute '("style" "width:1px")) narrow)) (define (add-indented-row indent label label-markup amount-indent rest) (when (or (not depth-limit) (<= indent depth-limit)) (gnc:html-table-append-row! table (append (if disable-account-indent? '() (make-list-thunk indent make-narrow-cell)) (list (if label-markup (gnc:make-html-table-cell/size/markup 1 (if disable-account-indent? 1 (- maxindent indent)) label-markup label) (gnc:make-html-table-cell/size 1 (if disable-account-indent? 1 (- maxindent indent)) label))) (gnc:html-make-empty-cells (if amount-indenting? (1- amount-indent) 0)) rest (gnc:html-make-empty-cells (if amount-indenting? (- maxindent amount-indent) 0)))))) (define (monetary+ . monetaries) ;; usage: (monetary+ monetary...) ;; inputs: list of gnc-monetary (e.g. USD 10, USD 25, GBP 5, GBP 8) ;; outputs: list of gnc-monetary (e.g. USD 35, GBP 13), or '() (let ((coll (gnc:make-commodity-collector))) (for-each (lambda (monetary) (if monetary (coll 'add (gnc:gnc-monetary-commodity monetary) (let ((amount (gnc:gnc-monetary-amount monetary))) (if negate-amounts? (- amount) amount))))) monetaries) (coll 'format gnc:make-gnc-monetary #f))) (define (list-of-monetary->html-text monetaries col-datum anchor) ;; inputs: ;; monetaries: list of gnc-monetary (or #f, or html-text object) ;; col-datum: col-datum to help convert monetary currency ;; anchor: url string for monetaries (or #f) (all have same anchor) ;; ;; outputs: html-text object (let ((text (gnc:make-html-text))) (for-each (lambda (monetary) (let ((converted (and show-orig-cur? convert-curr-fn (convert-curr-fn monetary col-datum)))) (if (not (and omit-zb-bals? (gnc:gnc-monetary? monetary) (zero? (gnc:gnc-monetary-amount monetary)))) (gnc:html-text-append! text (if converted (gnc:html-markup-i (gnc:html-markup "small" monetary " ")) "") (if anchor (gnc:html-markup-anchor anchor (or converted monetary)) (or converted monetary)) (gnc:html-markup-br))))) monetaries) text)) (define (render-account account total?) ;; input: account-name ;; outputs: string or html-markup-anchor object (let* ((acct-name ((if disable-account-indent? gnc-account-get-full-name xaccAccountGetName) account)) (acct-label (if total? (string-append (_ "Total For ") acct-name) acct-name)) (acct-url (and account-anchor? (not total?) (not (xaccAccountGetPlaceholder account)) (gnc:account-anchor-text account)))) (gnc:make-html-text (if acct-url (gnc:html-markup-anchor acct-url acct-label) acct-label)))) (define (add-whole-line contents) (gnc:html-table-append-row! table (gnc:make-html-table-cell/size 1 (+ 1 (if disable-account-indent? 0 maxindent) num-columns) contents))) (define (account-and-descendants account) (cons account (filter (lambda (acc) (member acc accountlist)) (gnc-account-get-descendants account)))) (define (sum-accounts-at-col accounts datum convert?) ;; outputs: list of gnc-monetary (apply monetary+ (map (lambda (acc) (let ((monetary (get-cell-monetary-fn acc datum))) (or (and convert? convert-curr-fn (convert-curr-fn monetary datum)) monetary))) accounts))) (define (is-not-zero? accts) ;; this function tests whether accounts (with descendants) of all ;; columns are zero. (not (every zero? (concatenate (map (lambda (col-datum) (map gnc:gnc-monetary-amount (sum-accounts-at-col accts col-datum #f))) cols-data))))) (define* (add-recursive-subtotal lvl lvl-acct #:key account-style-normal?) (if (or show-zb-accts? (is-not-zero? (account-and-descendants lvl-acct))) (add-indented-row lvl (render-account lvl-acct (not account-style-normal?)) (if account-style-normal? "text-cell" "total-label-cell") (- maxindent lvl) (map (lambda (col-datum) (gnc:make-html-table-cell/markup "total-number-cell" (list-of-monetary->html-text (sum-accounts-at-col (account-and-descendants lvl-acct) col-datum #t) col-datum #f))) cols-data)))) (define* (add-account-row lvl-curr curr #:key (override-show-zb-accts? #f) (account-indent 0)) (if (or show-zb-accts? override-show-zb-accts? (is-not-zero? (list curr))) (add-indented-row lvl-curr (render-account curr #f) "text-cell" (- maxindent lvl-curr account-indent) (map (lambda (col-datum) (gnc:make-html-table-cell/markup "number-cell" (list-of-monetary->html-text (sum-accounts-at-col (list curr) col-datum (not show-orig-cur?)) col-datum (and get-cell-anchor-fn (get-cell-anchor-fn curr col-datum))))) cols-data)))) ;; header ASSET/LIABILITY etc (if show-title? (add-indented-row 0 title "total-label-cell" maxindent (if get-col-header-fn (map (lambda (col-datum) (get-col-header-fn accountlist col-datum)) cols-data) (gnc:html-make-empty-cells num-columns)))) (let loop ((accounts (if show-accounts? accountlist '()))) (if (pair? accounts) (let* ((curr (car accounts)) (rest (cdr accounts)) (next (and (pair? rest) (car rest))) (lvl-curr (gnc-account-get-current-depth curr)) (lvl-next (if next (gnc-account-get-current-depth next) 0)) (curr-descendants-list (filter (lambda (acc) (member acc accountlist)) (gnc-account-get-descendants curr))) (recursive-parent-acct? (and recursive-bals? (pair? curr-descendants-list))) (multilevel-parent-acct? (and (not recursive-bals?) (pair? curr-descendants-list)))) (if recursive-parent-acct? (begin (add-recursive-subtotal lvl-curr curr #:account-style-normal? #t) (if (is-not-zero? (list curr)) (add-account-row (1+ lvl-curr) curr #:override-show-zb-accts? #t))) (add-account-row lvl-curr curr #:account-indent (if multilevel-parent-acct? 1 0) #:override-show-zb-accts? multilevel-parent-acct?)) (if (and (not recursive-bals?) (> lvl-curr lvl-next)) (let multilevel-loop ((lvl (1- lvl-curr)) (lvl-acct (gnc-account-get-parent curr))) (unless (or (zero? lvl) (not (member lvl-acct accountlist)) (< lvl lvl-next)) (add-recursive-subtotal lvl lvl-acct) (multilevel-loop (1- lvl) (gnc-account-get-parent lvl-acct))))) (loop rest)))) (if show-total? (add-indented-row 0 (string-append (_ "Total For ") title) "total-label-cell" maxindent (map (lambda (col-datum) (let ((total-cell (gnc:make-html-table-cell/markup "total-number-cell" (list-of-monetary->html-text (sum-accounts-at-col accountlist col-datum #t) col-datum #f)))) (gnc:html-table-cell-set-style! total-cell "total-number-cell" 'attribute '("style" "border-top-style:solid; border-top-width: 1px; border-bottom-style:double")) total-cell)) cols-data))) (add-whole-line #f)) (define (monetary-less . monetaries) ;; syntax: (monetary-less mon0 mon1 mon2 ...) ;; equiv: (- mon0 mon1 mon2 ...) (let ((res (gnc:make-commodity-collector))) (res 'add (gnc:gnc-monetary-commodity (car monetaries)) (gnc:gnc-monetary-amount (car monetaries))) (for-each (lambda (mon) (res 'add (gnc:gnc-monetary-commodity mon) (- (gnc:gnc-monetary-amount mon)))) (cdr monetaries)) (car (res 'format gnc:make-gnc-monetary #f)))) ;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; multicol-report-renderer ;;;;;;;;;;;;;;;;;;;;;;;;;;; (define (multicol-report-renderer report-obj report-type) (define (get-option pagename optname) (gnc:option-value (gnc:lookup-option (gnc:report-options report-obj) pagename optname))) (gnc:report-starting (get-option gnc:pagename-general gnc:optname-reportname)) ;; get all options values (let* ((report-title (get-option gnc:pagename-general gnc:optname-reportname)) (company-name (get-option gnc:pagename-general optname-company-name)) (startdate (gnc:date-option-absolute-time (get-option gnc:pagename-general optname-startdate))) (enddate (gnc:date-option-absolute-time (get-option gnc:pagename-general optname-enddate))) (disable-account-indent? (get-option gnc:pagename-display optname-account-full-name)) (incr (let ((period (get-option gnc:pagename-general optname-period))) (and period (keylist-get-info periodlist period 'delta)))) (disable-amount-indent? (and (not incr) (get-option gnc:pagename-general optname-disable-amount-indent))) (enable-dual-columns? (and (not incr) (get-option gnc:pagename-general optname-dual-columns))) (accounts (get-option gnc:pagename-accounts optname-accounts)) (depth-limit (let ((limit (get-option gnc:pagename-accounts optname-depth-limit))) (and (not (eq? limit 'all)) limit))) (show-zb-accts? (get-option gnc:pagename-display optname-show-zb-accts)) (omit-zb-bals? (get-option gnc:pagename-display optname-omit-zb-bals)) (recursive-bals? (get-option gnc:pagename-display optname-parent-balance-mode)) (label-sections? (get-option gnc:pagename-display optname-label-sections)) (total-sections? (get-option gnc:pagename-display optname-total-sections)) (use-links? (get-option gnc:pagename-display optname-account-links)) (use-amount-links? (get-option gnc:pagename-display optname-amount-links)) (include-chart? (get-option gnc:pagename-general optname-include-chart)) (common-currency (and (get-option pagename-commodities optname-common-currency) (get-option pagename-commodities optname-report-commodity))) (has-price? (lambda (commodity) ;; the following tests whether an amount in commodity can be converted to ;; common-currency. if conversion successful, it will be a non-zero value. ;; note if we use API gnc-pricedb-has-prices, we're only querying the pricedb. ;; if we use gnc-pricedb-convert-balance-latest-price, we can potentially ;; use an intermediate currency. (not (zero? (gnc-pricedb-convert-balance-latest-price (gnc-pricedb-get-db (gnc-get-current-book)) (gnc-commodity-get-fraction commodity) commodity common-currency))))) (price-source (get-option pagename-commodities optname-price-source)) (report-dates (map (if (eq? report-type 'balsheet) gnc:time64-end-day-time gnc:time64-start-day-time) (if incr (gnc:make-date-list startdate enddate incr) (if (eq? report-type 'balsheet) (list enddate) (list startdate enddate))))) (accounts-balances (map (lambda (acc) (cons acc (gnc:account-get-balances-at-dates acc report-dates))) accounts)) (convert-curr-fn (lambda (monetary col-idx) (and common-currency (not (gnc-commodity-equal (gnc:gnc-monetary-commodity monetary) common-currency)) (has-price? (gnc:gnc-monetary-commodity monetary)) (let* ((date (case price-source ((startperiod) startdate) ((midperiod) (floor (/ (+ startdate enddate) 2))) ((endperiod weighted-average average-cost) enddate) ((pricedb-latest) (current-time)) (else (list-ref report-dates (case report-type ((balsheet) col-idx) ((pnl) (1+ col-idx))))))) (exchange-fn (gnc:case-exchange-fn (if (memq price-source '(startperiod midperiod endperiod)) 'pricedb-nearest price-source) common-currency date))) (exchange-fn monetary common-currency))))) ;; the following function generates an gnc:html-text object ;; to dump exchange rate for a particular column. From the ;; accountlist given, obtain commodities, and convert 1 unit ;; currency into report-currency. If cannot convert due to ;; missing price, say so. (get-exchange-rates-fn (lambda (accounts date) (let ((commodities (delete common-currency (delete-duplicates (map xaccAccountGetCommodity accounts) gnc-commodity-equal) gnc-commodity-equal)) (cell (gnc:make-html-text))) (for-each (lambda (commodity) (let ((orig-monetary (gnc:make-gnc-monetary commodity 1))) (if (has-price? commodity) (let ((conv-monetary (convert-curr-fn orig-monetary (case report-type ((balsheet) date) ((pnl) (cons startdate enddate)))))) (gnc:html-text-append! cell (format #f "~a ~a" (gnc:monetary->string orig-monetary) (gnc:monetary->string conv-monetary)))) (gnc:html-text-append! cell (format #f (string-append "~a ~a" (_ "missing")) (gnc:monetary->string orig-monetary) (gnc-commodity-get-nice-symbol common-currency))))) (gnc:html-text-append! cell (gnc:html-markup-br))) commodities) (gnc:make-html-table-cell/markup "number-cell" cell)))) ;; decompose the account list (show-foreign? (get-option pagename-commodities optname-show-foreign)) (show-rates? (get-option pagename-commodities optname-show-rates)) (split-up-accounts (gnc:decompose-accountlist accounts)) (asset-accounts (assoc-ref split-up-accounts ACCT-TYPE-ASSET)) (liability-accounts (assoc-ref split-up-accounts ACCT-TYPE-LIABILITY)) (income-accounts (assoc-ref split-up-accounts ACCT-TYPE-INCOME)) (expense-accounts (assoc-ref split-up-accounts ACCT-TYPE-EXPENSE)) (equity-accounts (assoc-ref split-up-accounts ACCT-TYPE-EQUITY)) (trading-accounts (assoc-ref split-up-accounts ACCT-TYPE-TRADING)) (doc (gnc:make-html-document)) (multicol-table-left (gnc:make-html-table)) (multicol-table-right (if enable-dual-columns? (gnc:make-html-table) multicol-table-left)) (maxindent (gnc-account-get-tree-depth (gnc-get-current-root-account)))) (gnc:html-document-set-title! doc (string-append company-name " " report-title " " (if (and (eq? report-type 'balsheet) (not incr)) "" (string-append (qof-print-date startdate) " - ")) (qof-print-date enddate))) (if (eq? (get-option gnc:pagename-general optname-options-summary) 'always) (gnc:html-document-add-object! doc (gnc:html-render-options-changed (gnc:report-options report-obj)))) (if (null? accounts) (gnc:html-document-add-object! doc (gnc:html-make-no-account-warning report-title (gnc:report-id report-obj))) (case report-type ((balsheet) (let* ((get-cell-monetary-fn (lambda (account col-idx) (let ((account-balance-list (assoc account accounts-balances))) (and account-balance-list (list-ref account-balance-list (1+ col-idx)))))) (get-cell-anchor-fn (lambda (account col-idx) (let* ((splits (xaccAccountGetSplitList account)) (split-date (lambda (s) (xaccTransGetDate (xaccSplitGetParent s)))) (date (list-ref report-dates col-idx)) (valid-split? (lambda (s) (< (split-date s) date))) (valid-splits (filter valid-split? splits)) (split (and (pair? valid-splits) (last valid-splits)))) (and split (gnc:split-anchor-text split))))) (chart (and include-chart? (gnc:make-report-anchor networth-barchart-uuid report-obj (list (list "General" "Start Date" (cons 'absolute startdate)) (list "General" "End Date" (cons 'absolute enddate)) (list "General" "Report's currency" (or common-currency (gnc-default-report-currency))) (list "General" "Price Source" price-source) (list "Accounts" "Accounts" (append asset-accounts liability-accounts)))))) (get-col-header-fn (lambda (accounts col-idx) (let* ((date (list-ref report-dates col-idx)) (header (qof-print-date date)) (cell (gnc:make-html-table-cell/markup "total-label-cell" header))) (gnc:html-table-cell-set-style! cell "total-label-cell" 'attribute '("style" "text-align:right")) cell))) (add-to-table (lambda* (table title accounts #:key (get-col-header-fn #f) (show-accounts? #t) (show-total? #t) (force-total? #f) (negate-amounts? #f)) (add-multicolumn-acct-table table title accounts maxindent get-cell-monetary-fn (iota (length report-dates)) #:omit-zb-bals? omit-zb-bals? #:show-zb-accts? show-zb-accts? #:disable-account-indent? disable-account-indent? #:negate-amounts? negate-amounts? #:disable-amount-indent? disable-amount-indent? #:depth-limit (if get-col-header-fn 0 depth-limit) #:show-orig-cur? show-foreign? #:show-title? label-sections? #:show-accounts? show-accounts? #:show-total? (or (and total-sections? show-total?) force-total?) #:recursive-bals? recursive-bals? #:account-anchor? use-links? #:convert-curr-fn (and common-currency convert-curr-fn) #:get-col-header-fn get-col-header-fn #:get-cell-anchor-fn (and use-amount-links? get-cell-anchor-fn) )))) (when incr (add-to-table multicol-table-left (_ "Date") '() #:get-col-header-fn get-col-header-fn #:show-accounts? #f #:show-total? #f) (if enable-dual-columns? (add-to-table multicol-table-right (_ "Date") '() #:get-col-header-fn get-col-header-fn #:show-accounts? #f #:show-total? #f))) (unless (null? asset-accounts) (add-to-table multicol-table-left (_ "Asset") asset-accounts)) (unless (null? liability-accounts) (add-to-table multicol-table-right (_ "Liability") liability-accounts #:negate-amounts? #t)) (unless (null? equity-accounts) (add-to-table multicol-table-right (_ "Equity") equity-accounts)) (unless (null? trading-accounts) (add-to-table multicol-table-right (_ "Trading") trading-accounts)) (unless (or (null? asset-accounts) (null? liability-accounts)) (add-to-table multicol-table-right (_ "Net Worth") (append asset-accounts liability-accounts trading-accounts) #:show-accounts? #f #:force-total? #t)) (if (and common-currency show-rates?) (add-to-table multicol-table-right (_ "Exchange Rates") (append asset-accounts liability-accounts) #:get-col-header-fn get-exchange-rates-fn #:show-accounts? #f #:show-total? #f)) (if include-chart? (gnc:html-document-add-object! doc (gnc:make-html-text (gnc:html-markup-anchor chart "Barchart")))))) ((pnl) (let* ((closing-str (get-option pagename-entries optname-closing-pattern)) (closing-cased (get-option pagename-entries optname-closing-casing)) (closing-regexp (get-option pagename-entries optname-closing-regexp)) (include-overall-period? (get-option gnc:pagename-general optname-include-overall-period)) (col-idx->datepair (lambda (idx) (if (eq? idx 'overall-period) (cons (car report-dates) (last report-dates)) (cons (list-ref report-dates idx) (list-ref report-dates (1+ idx)))))) (col-idx->monetarypair (lambda (balancelist idx) (if (eq? idx 'overall-period) (cons (car balancelist) (last balancelist)) (cons (list-ref balancelist idx) (list-ref balancelist (1+ idx)))))) (closing-entries (let ((query (qof-query-create-for-splits))) (qof-query-set-book query (gnc-get-current-book)) (xaccQueryAddAccountMatch query (append income-accounts expense-accounts) QOF-GUID-MATCH-ANY QOF-QUERY-AND) (if (and closing-str (not (string-null? closing-str))) (xaccQueryAddDescriptionMatch query closing-str closing-cased closing-regexp QOF-COMPARE-CONTAINS QOF-QUERY-AND)) (xaccQueryAddClosingTransMatch query #t QOF-QUERY-OR) (let ((splits (qof-query-run query))) (qof-query-destroy query) splits))) ;; this function will query the above closing-entries for splits within the date range, ;; and produce the total amount for these closing entries (closing-adjustment (lambda (account col-idx) (define datepair (col-idx->datepair col-idx)) (define (include-split? split) (and (equal? (xaccSplitGetAccount split) account) (<= (car datepair) (xaccTransGetDate (xaccSplitGetParent split)) (cdr datepair)))) (let ((account-closing-splits (filter include-split? closing-entries))) (gnc:make-gnc-monetary (xaccAccountGetCommodity account) (apply + (map xaccSplitGetAmount account-closing-splits)))))) (get-cell-monetary-fn (lambda (account col-idx) (let ((account-balance-list (assoc account accounts-balances))) (and account-balance-list (let ((monetarypair (col-idx->monetarypair (cdr account-balance-list) col-idx))) (monetary-less (cdr monetarypair) (car monetarypair) (closing-adjustment account col-idx))))))) (get-cell-anchor-fn (lambda (account col-idx) (define datepair (col-idx->datepair col-idx)) (gnc:make-report-anchor trep-uuid report-obj (list (list "General" "Start Date" (cons 'absolute (car datepair))) (list "General" "End Date" (cons 'absolute (cdr datepair))) (list "Accounts" "Accounts" (list account)))))) (chart (and include-chart? (gnc:make-report-anchor pnl-barchart-uuid report-obj (list (list "General" "Start Date" (cons 'absolute startdate)) (list "General" "End Date" (cons 'absolute enddate)) (list "General" "Report's currency" (or common-currency (gnc-default-report-currency))) (list "General" "Price Source" (case price-source ((pricedb-latest) 'pricedb-latest) (else 'pricedb-nearest))) (list "Accounts" "Accounts" (append income-accounts expense-accounts)))))) (get-col-header-fn (lambda (accounts col-idx) (let* ((datepair (col-idx->datepair col-idx)) (header (gnc:make-html-text (qof-print-date (car datepair)) (gnc:html-markup-br) (_ " to ") (qof-print-date (cdr datepair)))) (cell (gnc:make-html-table-cell/markup "total-label-cell" header))) (gnc:html-table-cell-set-style! cell "total-label-cell" 'attribute '("style" "text-align:right")) cell))) (add-to-table (lambda* (table title accounts #:key (get-col-header-fn #f) (show-accounts? #t) (show-total? #t) (force-total? #f) (negate-amounts? #f)) (add-multicolumn-acct-table table title accounts maxindent get-cell-monetary-fn (append (iota (1- (length report-dates))) (if (and include-overall-period? (> (length report-dates) 2)) '(overall-period) '())) #:omit-zb-bals? omit-zb-bals? #:show-zb-accts? show-zb-accts? #:disable-account-indent? disable-account-indent? #:negate-amounts? negate-amounts? #:disable-amount-indent? disable-amount-indent? #:depth-limit (if get-col-header-fn 0 depth-limit) #:show-orig-cur? show-foreign? #:show-title? label-sections? #:show-accounts? show-accounts? #:show-total? (or (and total-sections? show-total?) force-total?) #:recursive-bals? recursive-bals? #:account-anchor? use-links? #:convert-curr-fn (and common-currency convert-curr-fn) #:get-col-header-fn get-col-header-fn #:get-cell-anchor-fn (and use-amount-links? get-cell-anchor-fn) )))) (when incr (add-to-table multicol-table-left (_ "Period") '() #:get-col-header-fn get-col-header-fn #:show-accounts? #f #:show-total? #f) (if enable-dual-columns? (add-to-table multicol-table-right (_ "Period") '() #:get-col-header-fn get-col-header-fn #:show-accounts? #f #:show-total? #f))) (unless (null? income-accounts) (add-to-table multicol-table-left (_ "Income") income-accounts #:negate-amounts? #t)) (unless (null? expense-accounts) (add-to-table multicol-table-right (_ "Expense") expense-accounts)) (unless (or (null? income-accounts) (null? expense-accounts)) (add-to-table multicol-table-left (_ "Net Income") (append income-accounts expense-accounts) #:show-accounts? #f #:negate-amounts? #t #:force-total? #t)) (if (and common-currency show-rates?) (add-to-table multicol-table-left (_ "Exchange Rates") (append income-accounts expense-accounts) #:get-col-header-fn get-exchange-rates-fn #:show-accounts? #f #:show-total? #f)) (if include-chart? (gnc:html-document-add-object! doc (gnc:make-html-text (gnc:html-markup-anchor chart "Barchart")))))))) (let ((multicol-table (if enable-dual-columns? (gnc:make-html-table) multicol-table-left))) (when enable-dual-columns? (gnc:html-table-append-row! multicol-table (list multicol-table-left multicol-table-right))) (gnc:html-document-add-object! doc multicol-table)) (gnc:html-document-add-object! doc (gnc:make-html-text FOOTER-TEXT)) (gnc:report-finished) ;; (gnc:html-document-set-style-text! ;; doc " table, td{ border-width: 1px; border-style:solid; border-color: lightgray; border-collapse: collapse}") doc)) (define balsheet-reportname (_ "Balance Sheet (Multicolumn)")) (define pnl-reportname (_ "Income Statement (Multicolumn)")) (gnc:define-report 'version 1 'name balsheet-reportname 'report-guid "065d5d5a77ba11e8b31e83ada73c5eea" 'menu-path (list gnc:menuname-asset-liability) 'options-generator (lambda () (multicol-report-options-generator 'balsheet)) 'renderer (lambda (rpt) (multicol-report-renderer rpt 'balsheet))) (gnc:define-report 'version 1 'name pnl-reportname 'report-guid "0e94fd0277ba11e8825d43e27232c9d4" 'menu-path (list gnc:menuname-income-expense) 'options-generator (lambda () (multicol-report-options-generator 'pnl)) 'renderer (lambda (rpt) (multicol-report-renderer rpt 'pnl))) ;; END