1 {-# LANGUAGE OverloadedLists #-}
2 {-# LANGUAGE PolyKinds #-}
4 {-# LANGUAGE UndecidableInstances #-}
5 {-# OPTIONS_GHC -Wno-orphans #-}
7 module Literate.Invoice.HTML where
9 import Data.List qualified as List
10 import Data.Map.Strict qualified as Map
11 import Data.Time qualified as Time
12 import Literate.Accounting.Math
13 import Literate.Accounting.Math qualified as Math
14 import Literate.Document.HTML
15 import Literate.Document.HTML qualified as HTML
16 import Literate.Document.Table qualified as Doc
17 import Literate.Document.Type qualified as Doc
18 import Literate.Invoice.Invoice
19 import Literate.Organization
20 import Literate.Prelude
21 import Paths_literate_invoice qualified as Self
22 import System.FilePath.Posix qualified as File
23 import Text.Printf qualified as Printf
24 import Prelude qualified
26 class HTMLIOable a where
27 htmlIO :: a -> IO Html
29 instance Doc.ToBlock InvoiceType where
31 InvoiceTypeProForma -> "pro forma"
32 InvoiceTypeSale -> "sale"
33 InvoiceTypeVoucher -> "voucher"
34 instance Doc.ToBlock InvoiceItemType where
36 InvoiceItemTypeItem -> "item"
37 InvoiceItemTypeService -> "service"
39 instance (ToMarkup (Quantity qf), UnitShowS unit) => ToMarkup (Amount qf unit) where
41 (amountQuantity & HTML.toMarkup)
42 <> (if null unit then "" else "\x202F" <> (unit & toHtml))
45 instance (Doc.ToInline (Quantity qf), UnitShowS unit) => Doc.ToInline (Amount qf unit) where
47 (amountQuantity & Doc.toInline)
48 <> (if null unit then "" else "\x202F" <> (unit & Doc.toInline))
51 instance (Doc.ToBlock (Quantity qf), QuantFact qf, UnitShowS unit) => Doc.ToBlock (Amount qf unit) where
52 toBlock x = Doc.BlockPara $ x & Doc.toInline
54 instance QuantFact qf => ToMarkup (Quantity qf) where
59 & (Prelude.toRational >>> Prelude.fromRational :: Ratio Natural -> Double)
60 & ( `Printf.formatArg`
63 , fmtAlternate = False
68 quantisationFactor @qf
69 & (Prelude.fromIntegral :: _ -> Double)
77 instance QuantFact qf => Doc.ToInline (Quantity qf) where
78 toInline qty = Doc.toInline do
81 & (Prelude.toRational >>> Prelude.fromRational :: Ratio Natural -> Double)
82 & ( `Printf.formatArg`
85 , fmtAlternate = False
90 quantisationFactor @qf
91 & (Prelude.fromIntegral :: _ -> Double)
99 instance QuantFact qf => Doc.ToBlock (Quantity qf) where
100 toBlock x = Doc.BlockPara $ x & Doc.toInline
102 instance HTMLIOable (InvoiceId, Invoice) where
103 htmlIO (invId, inv) = do
104 -- FixMe(portability): this absolute path is not portable out of my system
105 dataPath <- Self.getDataDir <&> File.normalise
106 -- paperCSS <- dataPath </> "styles" </> "Paper.css" & BS.readFile <&> Text.decodeUtf8
107 -- invoiceCSS <- dataPath </> "styles" </> "Invoice.css" & BS.readFile <&> Text.decodeUtf8
108 let invSummary :: [InvoiceItem _ _] =
109 invoiceSummary (inv & invoiceRates) (inv & invoiceLogs)
110 & foldMap (foldMap pure)
111 let invSummaryQuantityTotal :: Math.Quantity 100 =
112 invSummary <&> invoiceItemQuantity <&> Math.amountQuantity & Math.sumQuantities & fromMaybe 0
113 let invTitle :: Doc.Inline =
115 List.intersperse " - " $
117 [ case inv & invoiceIssuer & orgEntity & entityName of
119 Just n -> [Doc.toInline n]
120 , [inv & invoiceEmittedOn & Doc.toInline]
121 , ["Invoice #" <> (invId & Doc.toInline)]
122 , inv & invoiceOrders
128 { documentTitle = invTitle
129 , documentAttachments =
131 [ "styles/Document.css"
132 , "styles/Invoice.css"
141 { Doc.pageSection = Just $ invTitle
144 { Doc.flexDirection = Doc.FlexDirectionColumn
145 , Doc.flexGap = 0.5 & Doc.cm
147 [ "Invoice" & Doc.classes ["title"]
149 ( [ ["InvoiceIdentifier" := invId & Doc.toInline & Doc.toBlock]
150 , ["InvoiceType" := invId & invoiceIdType & Doc.toBlock]
151 , ["InvoiceOrders" := inv & invoiceOrders <&> (\x -> "-" := Doc.toBlock x) & Doc.List & Doc.toBlock]
152 , ["InvoiceEmittedOn" := inv & invoiceEmittedOn & Doc.toBlock]
153 , ["InvoicePaymentDueBefore" := inv & invoicePaymentDueBefore & Doc.toBlock]
159 & (\x -> x{Doc.containerClasses = ["invoice-headers"]})
164 [ "Seller" := inv & invoiceIssuer
165 , "Buyer" := inv & invoiceRecipient
171 x{Doc.containerClasses = ["invoice-from-to"]}
174 [ "Grand totals" & Doc.classes ["title"]
180 , "To pay (excl. taxes)"
185 { Doc.tableCellJustify = Doc.JustifyCenter
186 , Doc.tableCellContent =
188 <&> invoiceItemPeriod
192 { periodBeginning = min (x & periodBeginning) (y & periodBeginning)
193 , periodEnd = max (x & periodEnd) (y & periodEnd)
197 [ period & periodBeginning & Doc.toInline & Doc.BlockPara
198 , period & periodEnd & Doc.toInline & Doc.BlockPara
202 { Doc.tableCellJustify = Doc.JustifyCenter
203 , Doc.tableCellContent =
205 [ itm & invoiceItemQuantity
212 { Doc.tableCellJustify = Doc.JustifyCenter
213 , Doc.tableCellContent =
215 [ itm & invoiceItemTotal
228 [ "Mandatory legal notices" & Doc.classes ["title"]
231 InvoiceMentionTVANonApplicable ->
232 -- "TVA non applicable, art. 293 B du code général des impôts."
233 "—" := "VAT not applicable, art. 293 B of the French General Tax Code."
234 InvoiceMentionIndemnitéForfaitaire ->
236 [ -- "Indemnité forfaitaire pour frais de recouvrement en cas de retard de paiement\x202F: "
237 "Fixed compensation for recovery costs in case of late payment: "
238 , (40 :: Math.Amount 100 (Math.UnitName "€"))
242 InvoiceMentionIndemnitéTaux rate ->
244 [ -- "Taux des pénalités pour retard de paiement exigibles à compter du "
245 "Late payment penalty rate (applicable from "
246 , inv & invoicePaymentDueBefore & Time.addLocalTime Time.nominalDay & Doc.toInline
249 , rate & Doc.toInline
250 , -- , " × montant impayé × nombre de jours de retard / 365.25"
251 " × unpaid amount × number of days late / 365.25"
254 | mention <- inv & invoiceMentions
260 <&> \blk -> Doc.flexItem{Doc.flexItemContent = blk}
265 , invSummary & ol1 & chunksOf 15 <&> \invItemsChunk ->
267 { Doc.pageSection = Just $ invTitle <> " — Summary"
270 { Doc.flexDirection = Doc.FlexDirectionColumn
271 , Doc.flexGap = 0.5 & Doc.cm
274 { Doc.flexItemContent =
275 [ [ ("Summary" :: Text) & Doc.toBlock & Doc.classes ["title"]
276 | invItemsChunk & headMaybe & maybe False (fst >>> (== 1))
281 { Doc.tableTemplate =
282 [ Doc.LengthRelative $ Doc.LengthRelativeMaxContent
283 , Doc.LengthRelative $ Doc.LengthRelativeMaxContent
284 , Doc.LengthRelative $ 1 & Doc.fr
285 , Doc.LengthRelative $ Doc.LengthRelativeMaxContent
286 , Doc.LengthRelative $ Doc.LengthRelativeMaxContent
287 , Doc.LengthRelative $ Doc.LengthRelativeMaxContent
288 , Doc.LengthRelative $ Doc.LengthRelativeMaxContent
289 , Doc.LengthRelative $ Doc.LengthRelativeMaxContent
291 , Doc.tableRowsEvenOdd = True
300 { Doc.tableCellContent =
301 [ "Rate" & Doc.BlockPara
302 , "(excl.\xA0taxes.)" & Doc.BlockPara
304 , Doc.tableCellJustify = Doc.JustifyCenter
308 { Doc.tableCellContent =
309 [ "Total" & Doc.BlockPara
310 , "(excl.\xA0taxes.)" & Doc.BlockPara
312 , Doc.tableCellJustify = Doc.JustifyCenter
317 { Doc.tableCellContent = itemCount & Printf.printf "%i" & Doc.toBlock @String
318 , Doc.tableCellJustify = Doc.JustifyEnd
321 { Doc.tableCellContent = invItem & invoiceItemType & Doc.toBlock
322 , Doc.tableCellJustify = Doc.JustifyCenter
325 { Doc.tableCellContent = invItem & invoiceItemScope & pathToBlock
326 , Doc.tableCellJustify = Doc.JustifyStart
329 { Doc.tableCellContent = invItem & invoiceItemAction & pathToBlock
330 , Doc.tableCellJustify = Doc.JustifyStart
333 { Doc.tableCellContent =
334 [ invItem & invoiceItemPeriod & periodBeginning & Doc.toInline & Doc.BlockPara
335 , invItem & invoiceItemPeriod & periodEnd & Doc.toInline & Doc.BlockPara
337 , Doc.tableCellJustify = Doc.JustifyStart
340 { Doc.tableCellContent = invItem & invoiceItemRate & Doc.toBlock
341 , Doc.tableCellJustify = Doc.JustifyEnd
344 { Doc.tableCellContent =
345 [ invItem & invoiceItemQuantity & Doc.toBlock
346 , let (qtyPercent, _actualRate) =
348 (invSummaryQuantityTotal & Math.quantityToRatio & (100 Prelude./))
349 (invItem & invoiceItemQuantity & Math.amountQuantity)
351 & Math.quantityToRational
352 & (fromRational :: _ -> Double)
353 & (Printf.printf "(%02f%%)" :: _ -> String)
356 , Doc.tableCellJustify = Doc.JustifyEnd
359 { Doc.tableCellContent = invItem & invoiceItemTotal & Doc.toBlock
360 , Doc.tableCellJustify = Doc.JustifyEnd
363 | (itemCount, invItem) <- invItemsChunk
375 x{Doc.containerClasses = ["invoice-summary"]}
379 , inv & invoiceLogs & ol1 & chunksOf 15 <&> \invLogsChunk ->
381 { Doc.pageSection = Just $ invTitle <> " — Details"
384 { Doc.flexDirection = Doc.FlexDirectionColumn
385 , Doc.flexGap = 0.5 & Doc.cm
388 { Doc.flexItemContent =
389 [ [ ("Details" :: Text) & Doc.toBlock & Doc.classes ["title"]
390 | invLogsChunk & headMaybe & maybe False (fst >>> (== 1))
394 { Doc.tableTemplate =
395 [ Doc.LengthRelative $ Doc.LengthRelativeMaxContent
396 , Doc.LengthRelative $ Doc.LengthRelativeMaxContent
397 , Doc.LengthRelative $ Doc.LengthRelativeMaxContent
398 , Doc.LengthRelative $ 1 & Doc.fr
399 , Doc.LengthRelative $ 1 & Doc.fr
400 , Doc.LengthAbsolute $ 5 & Doc.cm
401 , Doc.LengthRelative $ 2 & Doc.fr
403 , Doc.tableRowsEvenOdd = True
416 { Doc.tableCellContent = logCount & Printf.printf "%i" & Doc.toBlock @String
417 , Doc.tableCellJustify = Doc.JustifyEnd
420 { Doc.tableCellContent = invoiceLog & invoiceLogDate & Doc.toBlock
421 , Doc.tableCellJustify = Doc.JustifyCenter
424 { Doc.tableCellContent =
425 invoiceLog & invoiceLogDuration & Doc.toBlock
426 , Doc.tableCellJustify = Doc.JustifyEnd
429 { Doc.tableCellContent = invoiceLog & invoiceLogScope & pathToBlock
430 , Doc.tableCellJustify = Doc.JustifyStart
433 { Doc.tableCellContent = invoiceLog & invoiceLogAction & pathToBlock
434 , Doc.tableCellJustify = Doc.JustifyStart
437 { Doc.tableCellContent =
439 | ref <- invoiceLog & invoiceLogReferences
443 , Doc.tableCellJustify = Doc.JustifyStart
446 { Doc.tableCellContent = invoiceLog & invoiceLogDescription & Doc.toBlock
447 , Doc.tableCellJustify = Doc.JustifyStart
450 | (logCount, invoiceLog) <- invLogsChunk & traceShowId
460 & (\x -> x{Doc.containerClasses = ["invoice-details"]})
467 l & List.map \(pageIdx, pag) ->
469 { Doc.pageOrientation = Doc.PageOrientationPortrait
470 , Doc.pageSize = Doc.PageSizeA4
471 , Doc.pageNumber = Just pageIdx
472 , Doc.pageNumberTotal = Just $ l & List.length & fromIntegral
476 then Doc.PageSideLeft
477 else Doc.PageSideRight
485 & List.intersperse " / "
489 invoiceSummary :: _ -> _ -> Map [Text] (Map [Text] (InvoiceItem _ _))
490 invoiceSummary invRates invLogs =
495 { invoiceItemScope = x & invoiceItemScope
496 , invoiceItemAction = x & invoiceItemAction
497 , invoiceItemPeriod =
499 { periodBeginning = min (x & invoiceItemPeriod & periodBeginning) (y & invoiceItemPeriod & periodBeginning)
500 , periodEnd = max (x & invoiceItemPeriod & periodEnd) (y & invoiceItemPeriod & periodEnd)
502 , invoiceItemQuantity = (x & invoiceItemQuantity) + (y & invoiceItemQuantity)
503 , invoiceItemType = x & invoiceItemType
504 , invoiceItemRate = x & invoiceItemRate
508 [ Map.singleton (invLog & invoiceLogScope) $
509 Map.singleton (invLog & invoiceLogAction) $
511 { invoiceItemScope = invLog & invoiceLogScope
512 , invoiceItemAction = invLog & invoiceLogAction
513 , invoiceItemType = InvoiceItemTypeService
514 , invoiceItemQuantity = invLog & invoiceLogDuration
515 , invoiceItemPeriod =
517 { periodBeginning = invLog & invoiceLogDate
518 , periodEnd = invLog & invoiceLogDate
520 , invoiceItemRate = invRates & Map.lookup (invLog & invoiceLogAction) & fromMaybe (errorShow ("missing action" :: Text, invLog & invoiceLogAction))