1 {-# LANGUAGE OverloadedLists #-}
2 {-# LANGUAGE PolyKinds #-}
4 {-# LANGUAGE UndecidableInstances #-}
5 {-# OPTIONS_GHC -Wno-orphans #-}
7 module Literate.Invoice.HTML where
9 import Data.Time.Format.ISO8601 qualified as Time
10 import Data.Time.LocalTime qualified as Time
11 import Literate.Accounting.Math
12 import Literate.Document qualified as Doc
13 import Literate.Document.HTML
14 import Literate.Invoice.Invoice
15 import Literate.Prelude
16 import Paths_literate_invoice qualified as Self
17 import System.FilePath.Posix ((</>))
18 import System.FilePath.Posix qualified as File
19 import Text.Blaze.Html5.Attributes qualified as HA
20 import Text.Printf qualified as Printf
21 import Prelude qualified
23 -- import Text.Blaze.Html5 qualified as H
24 class HTMLIOable a where
25 htmlIO :: a -> IO Html
27 instance Doc.ToBlock Address where
34 | t <- addr & addressText
38 [ addr & addressZipCode & Doc.toBlock
39 , addr & addressCity & Doc.toBlock
40 , addr & addressCountry & Doc.toBlock
44 instance Doc.ToBlock Time.LocalTime where
45 toBlock t = t & Time.localDay & Time.iso8601Show & fromString
48 instance ToMarkup Address where
49 toMarkup Address{..} = do
50 div ! classes ["address"] $ do
51 forM_ addressText \t ->
53 div ! classes ["address-bottom"] $ do
54 div $ addressZipCode & toHtml
55 div $ addressCity & toHtml
56 div $ addressCountry & toHtml
57 instance ToMarkup (String, Entity) where
58 toMarkup (pos, Entity{..}) = do
59 div ! classes ["entity"] $ do
60 div ! classes ["key-value", "entity-name"] $ do
61 div ! classes ["key"] $ do
63 ("\x202F:" :: String) & toHtml
64 div ! classes ["value"] $ do
66 div ! classes ["entity-address"] $ do
67 entityAddress & toHtml
71 div ! classes ["key-value", "entity-siren"] $ do
72 div ! classes ["key"] $ do
73 ("SIREN\x202F:" :: String) & toHtml
74 div ! classes ["value"] $ do
79 div ! classes ["key-value", "entity-email"] $ do
80 div ! classes ["key"] $ do
81 ("Email\x202F:" :: String) & toHtml
82 div ! classes ["value"] $ do
83 a ! HA.href ("mailto:" <> toValue email) $ do
86 instance Doc.ToBlock (String, Entity) where
91 { Doc.divClasses = ["entity-name"]
94 [ pos & Doc.toInline := ent & entityName & Doc.toBlock
98 { Doc.divClasses = ["entity-address"]
99 , Doc.divBlock = [ent & entityAddress & Doc.toBlock]
107 [ [ "SIREN" := siren & Doc.toBlock
108 | siren <- ent & entitySIREN & maybeToList
110 , -- FixMe: a ! HA.href ("mailto:" <> toValue email)
113 { Doc.inlineLinkText = email & Doc.toInline
114 , Doc.inlineLinkTarget = Doc.Target $ "mailto:" <> email
117 | email <- ent & entityEmail & maybeToList
124 instance (ToMarkup (Quantity qf), UnitShowS unit) => ToMarkup (Amount qf unit) where
125 toMarkup Amount{..} =
126 (amountQuantity & toMarkup)
127 <> (if null unit then "" else "\x202F" <> (unit & toHtml))
129 unit = unitShow @unit
130 instance (Doc.ToInline (Quantity qf), UnitShowS unit) => Doc.ToInline (Amount qf unit) where
131 toInline Amount{..} =
132 (amountQuantity & Doc.toInline)
133 <> (if null unit then "" else "\x202F" <> (unit & Doc.toInline))
135 unit = unitShow @unit
136 instance (Doc.ToBlock (Quantity qf), QuantFact qf, UnitShowS unit) => Doc.ToBlock (Amount qf unit) where
137 toBlock x = Doc.BlockPara $ x & Doc.toInline
139 instance QuantFact qf => ToMarkup (Quantity qf) where
143 & quantityToRatio @qf
144 & (Prelude.toRational >>> Prelude.fromRational :: Ratio Natural -> Double)
145 & ( `Printf.formatArg`
147 { fmtAdjust = Nothing
148 , fmtAlternate = False
153 quantisationFactor @qf
154 & (Prelude.fromIntegral :: _ -> Double)
162 instance QuantFact qf => Doc.ToInline (Quantity qf) where
163 toInline qty = Doc.toInline do
165 & quantityToRatio @qf
166 & (Prelude.toRational >>> Prelude.fromRational :: Ratio Natural -> Double)
167 & ( `Printf.formatArg`
169 { fmtAdjust = Nothing
170 , fmtAlternate = False
175 quantisationFactor @qf
176 & (Prelude.fromIntegral :: _ -> Double)
184 instance QuantFact qf => Doc.ToBlock (Quantity qf) where
185 toBlock x = Doc.BlockPara $ x & Doc.toInline
187 instance HTMLIOable (InvoiceId, Invoice) where
188 htmlIO (invId, inv) = do
189 -- FIXME: this absolute path is not portable out of my system
190 dataPath <- Self.getDataDir <&> File.normalise
191 -- paperCSS <- dataPath </> "styles" </> "Paper.css" & BS.readFile <&> Text.decodeUtf8
192 -- invoiceCSS <- dataPath </> "styles" </> "Invoice.css" & BS.readFile <&> Text.decodeUtf8
198 ( [ "styles/Document.css"
199 , "styles/Invoice.css"
208 ! HA.rel "stylesheet"
209 ! HA.type_ "text/css"
210 ! HA.href (dataPath </> cssFile & toValue)
211 -- styleCSS $ cssPrintPage pageOrientation pageSize
212 -- styleCSS $ pagesDifficulties & difficultyCSS
215 ! classes ["A4", "portrait", "sheet"]
216 ! styles ["size" := "A4 portrait"]
221 { Doc.flexDirection = Doc.FlexDirectionColumn
222 , Doc.flexGap = 0.5 & Doc.cm
225 [ "Invoice" := invId & Doc.toInline & Doc.BlockPara
226 , "IssueDate" := inv & invoiceCreation & Doc.toBlock
231 [ Doc.toBlock $ ("Seller" :: String) := inv & invoiceIssuer
232 , Doc.toBlock $ ("Buyer" :: String) := inv & invoiceCustomer
237 { Doc.tableTemplate =
238 [ Doc.LengthRelative $ Doc.LengthRelativeMaxContent
239 , Doc.LengthRelative $ 2 & Doc.fr
240 , Doc.LengthRelative $ 1 & Doc.fr
241 , Doc.LengthRelative $ 1 & Doc.fr
242 , Doc.LengthRelative $ 1 & Doc.fr
243 , Doc.LengthRelative $ 1 & Doc.fr
244 , Doc.LengthRelative $ 1 & Doc.fr
246 , Doc.tableRowsEvenOdd = True
249 [ Doc.tableCell{Doc.tableCellContent = "#"}
250 , Doc.tableCell{Doc.tableCellContent = "Description"}
251 , Doc.tableCell{Doc.tableCellContent = "Begin"}
252 , Doc.tableCell{Doc.tableCellContent = "End"}
253 , Doc.tableCell{Doc.tableCellContent = "Rate (excl.\xA0taxes.)"}
254 , Doc.tableCell{Doc.tableCellContent = "Quantity"}
255 , Doc.tableCell{Doc.tableCellContent = "Total (excl.\xA0taxes.)"}
259 { Doc.tableCellContent = itemCount & Doc.toBlock
260 , Doc.tableCellJustify = Doc.JustificationEnd
263 { Doc.tableCellContent = invoiceItem & invoiceItemDescription
264 , Doc.tableCellJustify = Doc.JustificationLeft
267 { Doc.tableCellContent = invoiceItem & invoiceItemPeriod & periodBegin & Doc.toBlock
268 , Doc.tableCellJustify = Doc.JustificationLeft
271 { Doc.tableCellContent = invoiceItem & invoiceItemPeriod & periodEnd & Doc.toBlock
272 , Doc.tableCellJustify = Doc.JustificationLeft
275 { Doc.tableCellContent = invoiceItem & invoiceItemRate & Doc.toBlock
276 , Doc.tableCellJustify = Doc.JustificationEnd
279 { Doc.tableCellContent = invoiceItem & invoiceItemQuantity & Doc.toBlock
280 , Doc.tableCellJustify = Doc.JustificationEnd
283 { Doc.tableCellContent = invoiceItem & invoiceItemTotal & Doc.toBlock
284 , Doc.tableCellJustify = Doc.JustificationEnd
287 | (itemCount, invoiceItem) <- inv & invoiceItems & ol1
295 [ Doc.tableCell{Doc.tableCellContent = "Total quantity"}
296 , Doc.tableCell{Doc.tableCellContent = "Total (excl. taxes)"}
301 { Doc.tableCellJustify = Doc.JustificationCenter
302 , Doc.tableCellContent =
304 [ itm & invoiceItemQuantity
305 | itm <- inv & invoiceItems
311 { Doc.tableCellJustify = Doc.JustificationCenter
312 , Doc.tableCellContent =
314 [ itm & invoiceItemTotal
315 | itm <- inv & invoiceItems
326 InvoiceMentionTVANonApplicable ->
327 "—" := "TVA non applicable, art. 293 B du code général des impôts"
328 | mention <- inv & invoiceMentions
332 <&> \blk -> (Doc.flexItem{Doc.flexItemContent = [blk]})