1 {-# LANGUAGE OverloadedLists #-}
2 {-# LANGUAGE PolyKinds #-}
4 {-# LANGUAGE UndecidableInstances #-}
5 {-# OPTIONS_GHC -Wno-operator-whitespace-ext-conflict #-}
6 {-# OPTIONS_GHC -Wno-orphans #-}
8 module Literate.Invoice.HTML where
10 import Data.Time.Format.ISO8601 qualified as Time
11 import Data.Time.LocalTime qualified as Time
12 import Literate.Accounting.Math
13 import Literate.Document qualified as Doc
14 import Literate.Document.HTML
15 import Literate.Document.Type (Block (BlockPara))
16 import Literate.Invoice.Invoice
17 import Literate.Prelude
18 import Paths_literate_invoice qualified as Self
19 import System.FilePath.Posix ((</>))
20 import System.FilePath.Posix qualified as File
21 import Text.Blaze.Html5.Attributes qualified as HA
22 import Text.Printf qualified as Printf
23 import Prelude qualified
25 -- import Text.Blaze.Html5 qualified as H
26 class HTMLIOable a where
27 htmlIO :: a -> IO Html
29 instance Doc.ToBlock Address where
32 { Doc.blockDivAnchor = Nothing
33 , Doc.blockDivClasses = ["address"]
38 | t <- addr & addressText
41 { Doc.blockDivAnchor = Nothing
42 , Doc.blockDivClasses = ["address-bottom"]
44 [ addr & addressZipCode & Doc.toBlock
45 , addr & addressCity & Doc.toBlock
46 , addr & addressCountry & Doc.toBlock
52 instance Doc.ToBlock Time.LocalTime where
53 toBlock t = t & Time.localDay & Time.iso8601Show & fromString
56 instance ToMarkup Address where
57 toMarkup Address{..} = do
58 div ! classes ["address"] $ do
59 forM_ addressText \t ->
61 div ! classes ["address-bottom"] $ do
62 div $ addressZipCode & toHtml
63 div $ addressCity & toHtml
64 div $ addressCountry & toHtml
65 instance ToMarkup (String, Entity) where
66 toMarkup (pos, Entity{..}) = do
67 div ! classes ["entity"] $ do
68 div ! classes ["key-value", "entity-name"] $ do
69 div ! classes ["key"] $ do
71 ("\x202F:" :: String) & toHtml
72 div ! classes ["value"] $ do
74 div ! classes ["entity-address"] $ do
75 entityAddress & toHtml
79 div ! classes ["key-value", "entity-siren"] $ do
80 div ! classes ["key"] $ do
81 ("SIREN\x202F:" :: String) & toHtml
82 div ! classes ["value"] $ do
87 div ! classes ["key-value", "entity-email"] $ do
88 div ! classes ["key"] $ do
89 ("Email\x202F:" :: String) & toHtml
90 div ! classes ["value"] $ do
91 a ! HA.href ("mailto:" <> toValue email) $ do
94 instance Doc.ToBlock (String, Entity) where
97 { Doc.blockDivAnchor = Nothing
98 , Doc.blockDivClasses = ["entity"]
101 { Doc.blockDivAnchor = Nothing
102 , Doc.blockDivClasses = ["entity-name"]
103 , Doc.blockDivBlock =
106 [ pos & Doc.toInline := ent & entityName & Doc.toBlock
109 { Doc.blockDivAnchor = Nothing
110 , Doc.blockDivClasses = ["entity-address"]
111 , Doc.blockDivBlock = [ent & entityAddress & Doc.toBlock]
118 [ [ "SIREN" := siren & Doc.toBlock
119 | siren <- ent & entitySIREN & maybeToList
121 , -- FixMe: a ! HA.href ("mailto:" <> toValue email)
124 { Doc.inlineLinkText = email & Doc.toInline
125 , Doc.inlineLinkTarget = Doc.Target $ "mailto:" <> email
128 | email <- ent & entityEmail & maybeToList
134 instance (ToMarkup (Quantity qf), UnitShowS unit) => ToMarkup (Amount qf unit) where
135 toMarkup Amount{..} =
136 (amountQuantity & toMarkup)
137 <> (if null unit then "" else "\x202F" <> (unit & toHtml))
139 unit = unitShow @unit
140 instance (Doc.ToInline (Quantity qf), UnitShowS unit) => Doc.ToInline (Amount qf unit) where
141 toInline Amount{..} =
142 (amountQuantity & Doc.toInline)
143 <> (if null unit then "" else "\x202F" <> (unit & Doc.toInline))
145 unit = unitShow @unit
146 instance (Doc.ToBlock (Quantity qf), QuantFact qf, UnitShowS unit) => Doc.ToBlock (Amount qf unit) where
147 toBlock x = Doc.BlockPara $ x & Doc.toInline
149 instance QuantFact qf => ToMarkup (Quantity qf) where
153 & quantityToRatio @qf
154 & (Prelude.toRational >>> Prelude.fromRational :: Ratio Natural -> Double)
155 & ( `Printf.formatArg`
157 { fmtAdjust = Nothing
158 , fmtAlternate = False
163 quantisationFactor @qf
164 & (Prelude.fromIntegral :: _ -> Double)
172 instance QuantFact qf => Doc.ToInline (Quantity qf) where
173 toInline qty = Doc.toInline do
175 & quantityToRatio @qf
176 & (Prelude.toRational >>> Prelude.fromRational :: Ratio Natural -> Double)
177 & ( `Printf.formatArg`
179 { fmtAdjust = Nothing
180 , fmtAlternate = False
185 quantisationFactor @qf
186 & (Prelude.fromIntegral :: _ -> Double)
194 instance QuantFact qf => Doc.ToBlock (Quantity qf) where
195 toBlock x = Doc.BlockPara $ x & Doc.toInline
197 instance HTMLIOable (InvoiceId, Invoice) where
198 htmlIO (invId, inv) = do
199 -- FIXME: this absolute path is not portable out of my system
200 dataPath <- Self.getDataDir <&> File.normalise
201 -- paperCSS <- dataPath </> "styles" </> "Paper.css" & BS.readFile <&> Text.decodeUtf8
202 -- invoiceCSS <- dataPath </> "styles" </> "Invoice.css" & BS.readFile <&> Text.decodeUtf8
208 ( [ "styles/Document.css"
209 , "styles/Invoice.css"
218 ! HA.rel "stylesheet"
219 ! HA.type_ "text/css"
220 ! HA.href (dataPath </> cssFile & toValue)
221 -- styleCSS $ cssPrintPage pageOrientation pageSize
222 -- styleCSS $ pagesDifficulties & difficultyCSS
225 ! classes ["A4", "portrait", "sheet"]
226 ! styles ["size" := "A4 portrait"]
229 { Doc.blockDivAnchor = Nothing
230 , Doc.blockDivClasses = ["invoice"]
231 , Doc.blockDivBlock =
234 { Doc.flexDirection = Doc.FlexDirectionColumn
235 , Doc.flexGap = 0.5 & Doc.cm
238 { Doc.blockDivAnchor = Nothing
239 , Doc.blockDivClasses = []
240 , Doc.blockDivBlock =
243 [ "Invoice" := invId & Doc.toInline & Doc.BlockPara
244 , "Date" := inv & invoiceCreation & Doc.toBlock
249 { Doc.blockDivAnchor = Nothing
250 , Doc.blockDivClasses = ["invoice-from-to"]
251 , Doc.blockDivBlock =
252 [ Doc.toBlock $ ("Seller" :: String) := inv & invoiceIssuer
253 , Doc.toBlock $ ("Buyer" :: String) := inv & invoiceCustomer
257 { Doc.blockDivAnchor = Nothing
258 , Doc.blockDivClasses = ["invoice-details"]
259 , Doc.blockDivBlock =
262 { Doc.tableTemplate =
263 [ Doc.LengthRelative $ Doc.LengthRelativeMaxContent
264 , Doc.LengthRelative $ 2 & Doc.fr
265 , Doc.LengthRelative $ 1 & Doc.fr
266 , Doc.LengthRelative $ 1 & Doc.fr
267 , Doc.LengthRelative $ 1 & Doc.fr
268 , Doc.LengthRelative $ 1 & Doc.fr
269 , Doc.LengthRelative $ 1 & Doc.fr
271 , Doc.tableRowsEvenOdd = True
274 { Doc.tableHeadColumns =
275 [ Doc.tableCell{Doc.tableCellContent = "#"}
276 , Doc.tableCell{Doc.tableCellContent = "Description"}
277 , Doc.tableCell{Doc.tableCellContent = "Begin"}
278 , Doc.tableCell{Doc.tableCellContent = "End"}
279 , Doc.tableCell{Doc.tableCellContent = "Rate (excl.\xA0taxes.)"}
280 , Doc.tableCell{Doc.tableCellContent = "Quantity"}
281 , Doc.tableCell{Doc.tableCellContent = "Total (excl.\xA0taxes.)"}
289 { Doc.tableCellContent = itemCount & Doc.toBlock
290 , Doc.tableCellJustify = Doc.JustificationEnd
293 { Doc.tableCellContent = invoiceItem & invoiceItemDescription
294 , Doc.tableCellJustify = Doc.JustificationLeft
297 { Doc.tableCellContent = invoiceItem & invoiceItemPeriod & periodBegin & Doc.toBlock
298 , Doc.tableCellJustify = Doc.JustificationLeft
301 { Doc.tableCellContent = invoiceItem & invoiceItemPeriod & periodEnd & Doc.toBlock
302 , Doc.tableCellJustify = Doc.JustificationLeft
305 { Doc.tableCellContent = invoiceItem & invoiceItemRate & Doc.toBlock
306 , Doc.tableCellJustify = Doc.JustificationEnd
309 { Doc.tableCellContent = invoiceItem & invoiceItemQuantity & Doc.toBlock
310 , Doc.tableCellJustify = Doc.JustificationEnd
313 { Doc.tableCellContent = invoiceItem & invoiceItemTotal & Doc.toBlock
314 , Doc.tableCellJustify = Doc.JustificationEnd
318 | (itemCount, invoiceItem) <- inv & invoiceItems & ol1
329 [ Doc.tableCell{Doc.tableCellContent = "Total quantity"}
330 , Doc.tableCell{Doc.tableCellContent = "Total (excl. taxes)"}
335 { Doc.tableRowColumns =
337 { Doc.tableCellJustify = Doc.JustificationCenter
338 , Doc.tableCellContent =
340 [ itm & invoiceItemQuantity
341 | itm <- inv & invoiceItems
347 { Doc.tableCellJustify = Doc.JustificationCenter
348 , Doc.tableCellContent =
350 [ itm & invoiceItemTotal
351 | itm <- inv & invoiceItems
363 InvoiceMentionTVANonApplicable ->
364 "—" := "TVA non applicable, art. 293 B du code général des impôts"
365 | mention <- inv & invoiceMentions
368 <&> \blk -> (Doc.flexItem{Doc.flexItemContent = [blk]})