cli: Commands.Balance.multiBalanceReportAsSpreadsheetHelper: vertically merge cells showing account names and Total
lib: Write.Spreadsheet: add support for cell spans
This commit is contained in:
parent
d12ec3b015
commit
5565f11c73
@ -46,18 +46,33 @@ formatCell cell =
|
|||||||
let class_ =
|
let class_ =
|
||||||
map (HtmlAttr.class_ . Html.textValue) $
|
map (HtmlAttr.class_ . Html.textValue) $
|
||||||
filter (not . Text.null) [Spr.textFromClass $ cellClass cell] in
|
filter (not . Text.null) [Spr.textFromClass $ cellClass cell] in
|
||||||
|
let span_ makeCell attrs =
|
||||||
|
case Spr.cellSpan cell of
|
||||||
|
Spr.NoSpan -> foldl (!) makeCell attrs
|
||||||
|
Spr.Covered -> pure ()
|
||||||
|
Spr.SpanHorizontal n ->
|
||||||
|
foldl (!) makeCell
|
||||||
|
(HtmlAttr.colspan (Html.stringValue $ show n) : attrs)
|
||||||
|
Spr.SpanVertical n ->
|
||||||
|
foldl (!) makeCell
|
||||||
|
(HtmlAttr.rowspan (Html.stringValue $ show n) : attrs)
|
||||||
|
in
|
||||||
case cellStyle cell of
|
case cellStyle cell of
|
||||||
Head -> foldl (!) (Html.th content) (style++class_)
|
Head -> span_ (Html.th content) (style++class_)
|
||||||
Body emph ->
|
Body emph ->
|
||||||
let align =
|
let align =
|
||||||
case cellType cell of
|
case cellType cell of
|
||||||
TypeString -> []
|
TypeString -> []
|
||||||
TypeDate -> []
|
TypeDate -> []
|
||||||
_ -> [HtmlAttr.align "right"]
|
_ -> [HtmlAttr.align "right"]
|
||||||
valign = [HtmlAttr.valign "top"]
|
valign =
|
||||||
|
case Spr.cellSpan cell of
|
||||||
|
Spr.SpanVertical n ->
|
||||||
|
if n>1 then [HtmlAttr.valign "top"] else []
|
||||||
|
_ -> []
|
||||||
withEmph =
|
withEmph =
|
||||||
case emph of
|
case emph of
|
||||||
Item -> id
|
Item -> id
|
||||||
Total -> Html.b
|
Total -> Html.b
|
||||||
in foldl (!) (Html.td $ withEmph content)
|
in span_ (Html.td $ withEmph content) $
|
||||||
(style++align++valign++class_)
|
style++align++valign++class_
|
||||||
|
|||||||
@ -45,16 +45,33 @@ formatCell cell =
|
|||||||
let class_ =
|
let class_ =
|
||||||
map Html.class_ $
|
map Html.class_ $
|
||||||
filter (not . Text.null) [Spr.textFromClass $ cellClass cell] in
|
filter (not . Text.null) [Spr.textFromClass $ cellClass cell] in
|
||||||
|
let span_ makeCell attrs cont =
|
||||||
|
case Spr.cellSpan cell of
|
||||||
|
Spr.NoSpan -> makeCell attrs cont
|
||||||
|
Spr.Covered -> pure ()
|
||||||
|
Spr.SpanHorizontal n ->
|
||||||
|
makeCell (Html.colspan_ (Text.pack $ show n) : attrs) cont
|
||||||
|
Spr.SpanVertical n ->
|
||||||
|
makeCell (Html.rowspan_ (Text.pack $ show n) : attrs) cont
|
||||||
|
in
|
||||||
case cellStyle cell of
|
case cellStyle cell of
|
||||||
Head -> Html.th_ (style++class_) content
|
Head -> span_ Html.th_ (style++class_) content
|
||||||
Body emph ->
|
Body emph ->
|
||||||
let align =
|
let align =
|
||||||
case cellType cell of
|
case cellType cell of
|
||||||
TypeString -> []
|
TypeString -> []
|
||||||
TypeDate -> []
|
TypeDate -> []
|
||||||
_ -> [HtmlBase.makeAttribute "align" "right"]
|
_ -> [HtmlBase.makeAttribute "align" "right"]
|
||||||
|
valign =
|
||||||
|
case Spr.cellSpan cell of
|
||||||
|
Spr.SpanVertical n ->
|
||||||
|
if n>1
|
||||||
|
then [HtmlBase.makeAttribute "valign" "top"]
|
||||||
|
else []
|
||||||
|
_ -> []
|
||||||
withEmph =
|
withEmph =
|
||||||
case emph of
|
case emph of
|
||||||
Item -> id
|
Item -> id
|
||||||
Total -> Html.b_
|
Total -> Html.b_
|
||||||
in Html.td_ (style++align++class_) $ withEmph content
|
in span_ Html.td_ (style++align++valign++class_) $
|
||||||
|
withEmph content
|
||||||
|
|||||||
@ -239,24 +239,32 @@ data DataStyle =
|
|||||||
|
|
||||||
cellConfig :: ((Spr.Border Spr.NumLines, Style), DataStyle) -> [String]
|
cellConfig :: ((Spr.Border Spr.NumLines, Style), DataStyle) -> [String]
|
||||||
cellConfig ((border, cstyle), dataStyle) =
|
cellConfig ((border, cstyle), dataStyle) =
|
||||||
let moreStyles =
|
let boldStyle = " <style:text-properties fo:font-weight='bold'/>"
|
||||||
|
alignTop =
|
||||||
|
" <style:table-cell-properties style:vertical-align='top'/>"
|
||||||
|
alignParagraph =
|
||||||
|
printf " <style:paragraph-properties fo:text-align='%s'/>"
|
||||||
|
moreStyles =
|
||||||
borderStyle border
|
borderStyle border
|
||||||
++
|
++
|
||||||
(
|
(
|
||||||
case cstyle of
|
case cstyle of
|
||||||
Body Item -> []
|
Body Item ->
|
||||||
|
alignTop :
|
||||||
|
[]
|
||||||
Body Total ->
|
Body Total ->
|
||||||
[" <style:text-properties fo:font-weight='bold'/>"]
|
alignTop :
|
||||||
|
boldStyle :
|
||||||
|
[]
|
||||||
Head ->
|
Head ->
|
||||||
" <style:paragraph-properties fo:text-align='center'/>" :
|
alignParagraph "center" :
|
||||||
" <style:text-properties fo:font-weight='bold'/>" :
|
boldStyle :
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
++
|
++
|
||||||
(
|
(
|
||||||
case dataStyle of
|
case dataStyle of
|
||||||
DataMixedAmount ->
|
DataMixedAmount -> [alignParagraph "end"]
|
||||||
[" <style:paragraph-properties fo:text-align='end'/>"]
|
|
||||||
_ -> []
|
_ -> []
|
||||||
)
|
)
|
||||||
cstyleName = cellStyleName cstyle
|
cstyleName = cellStyleName cstyle
|
||||||
@ -314,6 +322,19 @@ formatCell cell =
|
|||||||
(cellContent cell)
|
(cellContent cell)
|
||||||
_ -> "office:value-type='string'"
|
_ -> "office:value-type='string'"
|
||||||
|
|
||||||
|
covered =
|
||||||
|
case cellSpan cell of
|
||||||
|
Spr.Covered -> "covered-"
|
||||||
|
_ -> ""
|
||||||
|
|
||||||
|
span_ =
|
||||||
|
case cellSpan cell of
|
||||||
|
Spr.SpanHorizontal n | n>1 ->
|
||||||
|
printf " table:number-columns-spanned='%d'" n
|
||||||
|
Spr.SpanVertical n | n>1 ->
|
||||||
|
printf " table:number-rows-spanned='%d'" n
|
||||||
|
_ -> ""
|
||||||
|
|
||||||
anchor text =
|
anchor text =
|
||||||
if T.null $ Spr.cellAnchor cell
|
if T.null $ Spr.cellAnchor cell
|
||||||
then text
|
then text
|
||||||
@ -321,10 +342,10 @@ formatCell cell =
|
|||||||
(escape $ T.unpack $ Spr.cellAnchor cell) text
|
(escape $ T.unpack $ Spr.cellAnchor cell) text
|
||||||
|
|
||||||
in
|
in
|
||||||
printf "<table:table-cell%s %s>" style valueType :
|
printf "<table:%stable-cell%s%s %s>" covered style span_ valueType :
|
||||||
printf "<text:p>%s</text:p>"
|
printf "<text:p>%s</text:p>"
|
||||||
(anchor $ escape $ T.unpack $ cellContent cell) :
|
(anchor $ escape $ T.unpack $ cellContent cell) :
|
||||||
"</table:table-cell>" :
|
printf "</table:%stable-cell>" covered :
|
||||||
[]
|
[]
|
||||||
|
|
||||||
escape :: String -> String
|
escape :: String -> String
|
||||||
|
|||||||
@ -8,6 +8,7 @@ module Hledger.Write.Spreadsheet (
|
|||||||
Emphasis(..),
|
Emphasis(..),
|
||||||
Cell(..),
|
Cell(..),
|
||||||
Class(Class), textFromClass,
|
Class(Class), textFromClass,
|
||||||
|
Span(..),
|
||||||
Border(..),
|
Border(..),
|
||||||
Lines(..),
|
Lines(..),
|
||||||
NumLines(..),
|
NumLines(..),
|
||||||
@ -23,6 +24,8 @@ import Hledger.Data.Types (Amount)
|
|||||||
import qualified Data.List as List
|
import qualified Data.List as List
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
|
|
||||||
|
import Prelude hiding (span)
|
||||||
|
|
||||||
|
|
||||||
data Type =
|
data Type =
|
||||||
TypeString
|
TypeString
|
||||||
@ -82,19 +85,59 @@ newtype Class = Class Text
|
|||||||
textFromClass :: Class -> Text
|
textFromClass :: Class -> Text
|
||||||
textFromClass (Class cls) = cls
|
textFromClass (Class cls) = cls
|
||||||
|
|
||||||
|
|
||||||
|
{- |
|
||||||
|
* 'NoSpan' means a single unmerged cell.
|
||||||
|
|
||||||
|
* 'Covered' is a cell if it is part of a horizontally or vertically merged cell.
|
||||||
|
We maintain these cells although they are ignored in HTML output.
|
||||||
|
In contrast to that, FODS can store covered cells
|
||||||
|
and allows to access the hidden cell content via formulas.
|
||||||
|
CSV does not support merged cells
|
||||||
|
and thus simply writes the content of covered cells.
|
||||||
|
Maintaining 'Covered' cells also simplifies transposing.
|
||||||
|
|
||||||
|
* @'SpanHorizontal' n@ denotes the first cell in a row
|
||||||
|
that is part of a merged cell.
|
||||||
|
The merged cell contains @n@ atomic cells, including the first one.
|
||||||
|
That is @SpanHorizontal 1@ is actually like @NoSpan@.
|
||||||
|
The content of this cell is shown as content of the merged cell.
|
||||||
|
|
||||||
|
* @'SpanVertical' n@ starts a vertically merged cell.
|
||||||
|
|
||||||
|
The writer functions expect consistent data,
|
||||||
|
that is, 'Covered' cells must actually be part of a merged cell
|
||||||
|
and merged cells must only cover 'Covered' cells.
|
||||||
|
-}
|
||||||
|
data Span =
|
||||||
|
NoSpan
|
||||||
|
| Covered
|
||||||
|
| SpanHorizontal Int
|
||||||
|
| SpanVertical Int
|
||||||
|
deriving (Eq)
|
||||||
|
|
||||||
|
transposeSpan :: Span -> Span
|
||||||
|
transposeSpan span =
|
||||||
|
case span of
|
||||||
|
NoSpan -> NoSpan
|
||||||
|
Covered -> Covered
|
||||||
|
SpanHorizontal n -> SpanVertical n
|
||||||
|
SpanVertical n -> SpanHorizontal n
|
||||||
|
|
||||||
data Cell border text =
|
data Cell border text =
|
||||||
Cell {
|
Cell {
|
||||||
cellType :: Type,
|
cellType :: Type,
|
||||||
cellBorder :: Border border,
|
cellBorder :: Border border,
|
||||||
cellStyle :: Style,
|
cellStyle :: Style,
|
||||||
|
cellSpan :: Span,
|
||||||
cellAnchor :: Text,
|
cellAnchor :: Text,
|
||||||
cellClass :: Class,
|
cellClass :: Class,
|
||||||
cellContent :: text
|
cellContent :: text
|
||||||
}
|
}
|
||||||
|
|
||||||
instance Functor (Cell border) where
|
instance Functor (Cell border) where
|
||||||
fmap f (Cell typ border style anchor class_ content) =
|
fmap f (Cell typ border style span anchor class_ content) =
|
||||||
Cell typ border style anchor class_ $ f content
|
Cell typ border style span anchor class_ $ f content
|
||||||
|
|
||||||
defaultCell :: (Lines border) => text -> Cell border text
|
defaultCell :: (Lines border) => text -> Cell border text
|
||||||
defaultCell text =
|
defaultCell text =
|
||||||
@ -102,6 +145,7 @@ defaultCell text =
|
|||||||
cellType = TypeString,
|
cellType = TypeString,
|
||||||
cellBorder = noBorder,
|
cellBorder = noBorder,
|
||||||
cellStyle = Body Item,
|
cellStyle = Body Item,
|
||||||
|
cellSpan = NoSpan,
|
||||||
cellAnchor = mempty,
|
cellAnchor = mempty,
|
||||||
cellClass = Class mempty,
|
cellClass = Class mempty,
|
||||||
cellContent = text
|
cellContent = text
|
||||||
@ -112,7 +156,10 @@ emptyCell = defaultCell mempty
|
|||||||
|
|
||||||
transposeCell :: Cell border text -> Cell border text
|
transposeCell :: Cell border text -> Cell border text
|
||||||
transposeCell cell =
|
transposeCell cell =
|
||||||
cell {cellBorder = transposeBorder $ cellBorder cell}
|
cell {
|
||||||
|
cellBorder = transposeBorder $ cellBorder cell,
|
||||||
|
cellSpan = transposeSpan $ cellSpan cell
|
||||||
|
}
|
||||||
|
|
||||||
transpose :: [[Cell border text]] -> [[Cell border text]]
|
transpose :: [[Cell border text]] -> [[Cell border text]]
|
||||||
transpose = List.transpose . map (map transposeCell)
|
transpose = List.transpose . map (map transposeCell)
|
||||||
|
|||||||
@ -260,6 +260,7 @@ module Hledger.Cli.Commands.Balance (
|
|||||||
,multiBalanceReportTableAsText
|
,multiBalanceReportTableAsText
|
||||||
,multiBalanceReportAsSpreadsheet
|
,multiBalanceReportAsSpreadsheet
|
||||||
,addTotalBorders
|
,addTotalBorders
|
||||||
|
,addRowSpanHeader
|
||||||
,simpleDateSpanCell
|
,simpleDateSpanCell
|
||||||
,RowClass(..)
|
,RowClass(..)
|
||||||
-- ** Tests
|
-- ** Tests
|
||||||
@ -458,12 +459,11 @@ budgetAverageClass rc =
|
|||||||
case rc of Value -> "budget rowaverage"; Total -> "budget colaverage"
|
case rc of Value -> "budget rowaverage"; Total -> "budget colaverage"
|
||||||
|
|
||||||
-- What to show as heading for the totals row in balance reports ?
|
-- What to show as heading for the totals row in balance reports ?
|
||||||
-- Currently nothing in terminal, Total: in html and xSV output.
|
-- Currently nothing in terminal, Total: in HTML, FODS and xSV output.
|
||||||
totalRowHeadingText = ""
|
totalRowHeadingText = ""
|
||||||
totalRowHeadingBudgetText = ""
|
totalRowHeadingSpreadsheet = "Total:"
|
||||||
totalRowHeadingHtml = "Total:"
|
totalRowHeadingBudgetText = ""
|
||||||
totalRowHeadingCsv = "Total:"
|
totalRowHeadingBudgetCsv = "Total:"
|
||||||
totalRowHeadingBudgetCsv = "Total:"
|
|
||||||
|
|
||||||
-- Single-column balance reports
|
-- Single-column balance reports
|
||||||
|
|
||||||
@ -663,6 +663,19 @@ addTotalBorders =
|
|||||||
rawTableContent :: [[Ods.Cell border text]] -> [[text]]
|
rawTableContent :: [[Ods.Cell border text]] -> [[text]]
|
||||||
rawTableContent = map (map Ods.cellContent)
|
rawTableContent = map (map Ods.cellContent)
|
||||||
|
|
||||||
|
addRowSpanHeader ::
|
||||||
|
Ods.Cell border text ->
|
||||||
|
[[Ods.Cell border text]] -> [[Ods.Cell border text]]
|
||||||
|
addRowSpanHeader header rows =
|
||||||
|
case rows of
|
||||||
|
[] -> []
|
||||||
|
[row] -> [header:row]
|
||||||
|
_ ->
|
||||||
|
zipWith (:)
|
||||||
|
(header{Ods.cellSpan = Ods.SpanVertical (length rows)} :
|
||||||
|
repeat header{Ods.cellSpan = Ods.Covered})
|
||||||
|
rows
|
||||||
|
|
||||||
setAccountAnchor ::
|
setAccountAnchor ::
|
||||||
Maybe Text -> [Text] -> Text -> Ods.Cell border text -> Ods.Cell border text
|
Maybe Text -> [Text] -> Text -> Ods.Cell border text -> Ods.Cell border text
|
||||||
setAccountAnchor base query acct cell =
|
setAccountAnchor base query acct cell =
|
||||||
@ -677,7 +690,7 @@ balanceReportAsSpreadsheet opts (items, total) =
|
|||||||
headers :
|
headers :
|
||||||
concatMap (\(a, _, _, b) -> rows Value a b) items ++
|
concatMap (\(a, _, _, b) -> rows Value a b) items ++
|
||||||
if no_total_ opts then []
|
if no_total_ opts then []
|
||||||
else addTotalBorders $ rows Total totalRowHeadingCsv total
|
else addTotalBorders $ rows Total totalRowHeadingSpreadsheet total
|
||||||
where
|
where
|
||||||
cell = Ods.defaultCell
|
cell = Ods.defaultCell
|
||||||
headers =
|
headers =
|
||||||
@ -694,14 +707,12 @@ balanceReportAsSpreadsheet opts (items, total) =
|
|||||||
(guard (rc==Value) >> balance_base_url_ opts)
|
(guard (rc==Value) >> balance_base_url_ opts)
|
||||||
(querystring_ opts) name $
|
(querystring_ opts) name $
|
||||||
cell $ accountNameDrop (drop_ opts) name in
|
cell $ accountNameDrop (drop_ opts) name in
|
||||||
|
addRowSpanHeader accountCell $
|
||||||
case layout_ opts of
|
case layout_ opts of
|
||||||
LayoutBare ->
|
LayoutBare ->
|
||||||
map (\a ->
|
map (\a -> [cell $ acommodity a, renderAmount rc $ mixedAmount a])
|
||||||
[accountCell,
|
|
||||||
cell $ acommodity a,
|
|
||||||
renderAmount rc $ mixedAmount a])
|
|
||||||
. amounts $ mixedAmountStripCosts ma
|
. amounts $ mixedAmountStripCosts ma
|
||||||
_ -> [[accountCell, renderAmount rc ma]]
|
_ -> [[renderAmount rc ma]]
|
||||||
|
|
||||||
renderAmount rc mixedAmt =
|
renderAmount rc mixedAmt =
|
||||||
wbToText <$> cellFromMixedAmount bopts (amountClass rc, mixedAmt)
|
wbToText <$> cellFromMixedAmount bopts (amountClass rc, mixedAmt)
|
||||||
@ -787,18 +798,17 @@ multiBalanceReportAsSpreadsheetHelper ishtml opts@ReportOpts{..} (PeriodicReport
|
|||||||
[hCell "rowtotal" "total" | row_total_] ++
|
[hCell "rowtotal" "total" | row_total_] ++
|
||||||
[hCell "rowaverage" "average" | average_]
|
[hCell "rowaverage" "average" | average_]
|
||||||
fullRowAsTexts row =
|
fullRowAsTexts row =
|
||||||
map (anchorCell:) $
|
addRowSpanHeader anchorCell $
|
||||||
rowAsText Value (dateSpanCell balance_base_url_ querystring_ acctName) row
|
rowAsText Value (dateSpanCell balance_base_url_ querystring_ acctName) row
|
||||||
where acctName = prrFullName row
|
where acctName = prrFullName row
|
||||||
anchorCell =
|
anchorCell =
|
||||||
setAccountAnchor balance_base_url_ querystring_ acctName $
|
setAccountAnchor balance_base_url_ querystring_ acctName $
|
||||||
accountCell $ accountNameDrop drop_ acctName
|
accountCell $ accountNameDrop drop_ acctName
|
||||||
totalrows
|
totalrows =
|
||||||
| no_total_ = []
|
if no_total_
|
||||||
| ishtml = zipWith (:) (accountCell totalRowHeadingHtml : repeat Ods.emptyCell) $
|
then []
|
||||||
rowAsText Total simpleDateSpanCell tr
|
else addRowSpanHeader (accountCell totalRowHeadingSpreadsheet) $
|
||||||
| otherwise = map (accountCell totalRowHeadingCsv :) $
|
rowAsText Total simpleDateSpanCell tr
|
||||||
rowAsText Total simpleDateSpanCell tr
|
|
||||||
rowAsText rc dsCell =
|
rowAsText rc dsCell =
|
||||||
let fmt = if ishtml then oneLineNoCostFmt else machineFmt
|
let fmt = if ishtml then oneLineNoCostFmt else machineFmt
|
||||||
in map (map (fmap wbToText)) .
|
in map (map (fmap wbToText)) .
|
||||||
|
|||||||
@ -376,7 +376,7 @@ compoundBalanceReportAsHtml ropts cbr =
|
|||||||
Total simpleDateSpanCell totalrow
|
Total simpleDateSpanCell totalrow
|
||||||
-- make a table of rendered lines of the report totals row
|
-- make a table of rendered lines of the report totals row
|
||||||
& map (map (fmap wbToText))
|
& map (map (fmap wbToText))
|
||||||
& zipWith (:) (Spr.defaultCell "Net:" : repeat Spr.emptyCell)
|
& addRowSpanHeader (Spr.defaultCell "Net:")
|
||||||
-- insert a headings column, with Net: on the first line only
|
-- insert a headings column, with Net: on the first line only
|
||||||
& addTotalBorders -- marking the first for special styling
|
& addTotalBorders -- marking the first for special styling
|
||||||
& map (Html.formatRow . map (fmap L.toHtml))
|
& map (Html.formatRow . map (fmap L.toHtml))
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user