github.com/benoitkugler/goacve@v0.0.0-20201217100549-151ce6e55dc8/server/recufiscal/recu.go (about) 1 package recufiscal 2 3 import ( 4 "bytes" 5 "fmt" 6 "os/exec" 7 "strconv" 8 "strings" 9 "time" 10 11 dm "github.com/benoitkugler/goACVE/server/core/datamodel" 12 13 "golang.org/x/text/encoding/unicode" 14 15 "github.com/benoitkugler/num2words" 16 17 rd "github.com/benoitkugler/goACVE/server/core/rawdata" 18 ) 19 20 const ( 21 ModelePath = "ressources/ModeleRecuFiscalEditable.pdf" 22 valeurChecked = "Oui" 23 24 fdfHeader = `%FDF-1.2 25 %,,oe" 26 1 0 obj 27 << 28 /FDF << /Fields [` 29 30 fdfFooter = `] 31 >> 32 >> 33 endobj 34 trailer 35 << 36 /Root 1 0 R 37 >> 38 %%EOF` 39 ) 40 41 var ( 42 // Indique l'identifiant des champs présents dans le modèle. 43 // A synchroniser avec `MODELE_PATH` 44 correspondancesChamps = struct { 45 numero string 46 infos infos 47 categorie string 48 utilitePublique utilitePublique 49 donateur donateur 50 don don 51 articleLoi, formeDon, natureDon map[string]string 52 modeDon map[rd.ModePaiment]string 53 date date 54 }{ 55 numero: "z1", 56 infos: infos{ 57 nom: "z2", 58 adresse: "z4", 59 codePostal: "z5", 60 ville: "z5b", 61 objectifL1: "z6", 62 objectifL2: "z7", 63 }, 64 categorie: "z9", 65 utilitePublique: utilitePublique{ 66 jourDecret: "d1", 67 moisDecret: "d2", 68 anneeDecret: "d3", 69 jourJournal: "d1b", 70 moisJournal: "d2b", 71 anneeJournal: "d3b", 72 }, 73 donateur: donateur{ 74 nom: "z29", 75 prenom: "z30", 76 adresse: "z31", 77 codePostal: "z32", 78 ville: "z33", 79 }, 80 don: don{ 81 montantChiffre: "z34", 82 montantLettre: "z35", 83 jourVersement: "z36", 84 moisVersement: "z37", 85 anneeVersement: "z38", 86 }, 87 articleLoi: map[string]string{ 88 "200": "z39", 89 "238": "z40", 90 "885": "z41", 91 }, 92 formeDon: map[string]string{ 93 "authentique": "z42", 94 "seing": "z43", 95 "manuel": "z44", 96 "autres": "z45", 97 }, 98 natureDon: map[string]string{ 99 "numeraire": "z46", 100 "titres": "z47", 101 "autres": "z48", 102 }, 103 modeDon: map[rd.ModePaiment]string{ 104 "esp": "z49", 105 "cheque": "z50", 106 "vir": "z51", 107 "cb": "z51", 108 "ancv": "z50", 109 }, 110 date: date{ 111 jour: "z52", 112 mois: "z53", 113 annee: "z54", 114 }, 115 } 116 ) 117 118 // Définit les valeurs propres à l'ACVE 119 const ( 120 ArticleLoi = "200" 121 FormeDon = "manuel" 122 NatureDon = "numeraire" 123 ) 124 125 var ( 126 Infos = infos{ 127 nom: "ACVE", 128 adresse: "La Maison du Rocher", 129 codePostal: "26160", 130 ville: "CHAMALOC", 131 objectifL1: "Créer et gérer des séjours pour enfants, adolescents et adultes.", 132 objectifL2: "Faire connaître, à travers des animations adaptées à l’âge des participants, les valeurs chrétiennes.", 133 } 134 UtilitePublique = utilitePublique{ 135 jourDecret: "5", 136 moisDecret: "1", 137 anneeDecret: "1957", 138 jourJournal: "29", 139 moisJournal: "1", 140 anneeJournal: "1957", 141 } 142 ) 143 144 type champPdf struct { 145 id, valeur string 146 } 147 148 type utilitePublique struct { 149 jourDecret string 150 moisDecret string 151 anneeDecret string 152 jourJournal string 153 moisJournal string 154 anneeJournal string 155 } 156 157 type infos struct { 158 nom string 159 adresse string 160 codePostal string 161 ville string 162 objectifL1 string 163 objectifL2 string 164 } 165 166 type donateur struct { 167 nom string 168 prenom string 169 adresse string 170 codePostal string 171 ville string 172 } 173 174 type don struct { 175 montantChiffre string 176 montantLettre string 177 jourVersement string 178 moisVersement string 179 anneeVersement string 180 } 181 182 type date struct { 183 jour string 184 mois string 185 annee string 186 } 187 188 func champsACVE() []champPdf { 189 champsInfos := correspondancesChamps.infos 190 champUP := correspondancesChamps.utilitePublique 191 return []champPdf{ 192 // ACVE 193 {id: champsInfos.nom, valeur: Infos.nom}, 194 {id: champsInfos.adresse, valeur: Infos.adresse}, 195 {id: champsInfos.codePostal, valeur: Infos.codePostal}, 196 {id: champsInfos.ville, valeur: Infos.ville}, 197 {id: champsInfos.objectifL1, valeur: Infos.objectifL1}, 198 {id: champsInfos.objectifL2, valeur: Infos.objectifL2}, 199 200 // CATEGORIE ASSO 201 {id: correspondancesChamps.categorie, valeur: valeurChecked}, 202 203 {id: champUP.anneeDecret, valeur: UtilitePublique.anneeDecret}, 204 {id: champUP.anneeJournal, valeur: UtilitePublique.anneeJournal}, 205 {id: champUP.jourDecret, valeur: UtilitePublique.jourDecret}, 206 {id: champUP.jourJournal, valeur: UtilitePublique.jourJournal}, 207 {id: champUP.moisDecret, valeur: UtilitePublique.moisDecret}, 208 {id: champUP.moisJournal, valeur: UtilitePublique.moisJournal}, 209 } 210 } 211 212 func champsDonateur(donateur donateur) []champPdf { 213 champsDonateur := correspondancesChamps.donateur 214 return []champPdf{ 215 {id: champsDonateur.nom, valeur: donateur.nom}, 216 {id: champsDonateur.prenom, valeur: donateur.prenom}, 217 {id: champsDonateur.adresse, valeur: donateur.adresse}, 218 {id: champsDonateur.codePostal, valeur: donateur.codePostal}, 219 {id: champsDonateur.ville, valeur: donateur.ville}, 220 } 221 } 222 223 // `don` représente une aggrégation de plusieurs dons. 224 func champsDon(don dm.RecuFiscal) []champPdf { 225 champsDon := correspondancesChamps.don 226 date := don.Date 227 228 montantLettre := num2words.EurosToWords(float64(don.Montant)) 229 return []champPdf{ 230 {id: champsDon.montantChiffre, valeur: fmt.Sprintf("%.2f", don.Montant)}, 231 {id: champsDon.montantLettre, valeur: montantLettre}, 232 {id: champsDon.jourVersement, valeur: strconv.Itoa(date.Day())}, 233 {id: champsDon.moisVersement, valeur: strconv.Itoa(int(date.Month()))}, 234 {id: champsDon.anneeVersement, valeur: strconv.Itoa(date.Year())}, 235 {id: correspondancesChamps.modeDon[don.Mode], valeur: valeurChecked}, 236 } 237 } 238 239 func champsTypeDon() []champPdf { 240 return []champPdf{ 241 {id: correspondancesChamps.articleLoi[ArticleLoi], valeur: valeurChecked}, 242 {id: correspondancesChamps.natureDon[NatureDon], valeur: valeurChecked}, 243 {id: correspondancesChamps.formeDon[FormeDon], valeur: valeurChecked}, 244 } 245 } 246 247 func champsDateEdition() []champPdf { 248 corresDate := correspondancesChamps.date 249 today := time.Now() 250 return []champPdf{ 251 {id: corresDate.jour, valeur: strconv.Itoa(today.Day())}, 252 {id: corresDate.mois, valeur: strconv.Itoa(int(today.Month()))}, 253 {id: corresDate.annee, valeur: strconv.Itoa(today.Year())}, 254 } 255 } 256 257 func forgeFDF(form []champPdf) string { 258 w := new(strings.Builder) 259 260 // Write the fdf header. 261 if _, err := fmt.Fprintln(w, fdfHeader); err != nil { 262 return "" 263 } 264 265 encoder := unicode.UTF16(unicode.BigEndian, unicode.UseBOM).NewEncoder() 266 267 // Write the form data. 268 for _, champ := range form { 269 utf16, _ := encoder.String(champ.valeur) 270 if _, err := fmt.Fprintf(w, "<< /T (%s) /V (%v)>>\n", champ.id, utf16); err != nil { 271 return "" 272 } 273 } 274 275 // Write the fdf footer. 276 if _, err := fmt.Fprintln(w, fdfFooter); err != nil { 277 return "" 278 } 279 280 return w.String() 281 } 282 283 // ParamsRecuFiscal encode les informations nécessaire à l'émission d'un reçu fiscal. 284 // Dans le cas de plusieurs dons, `Montant` est le total des dons, et `Mode` est 285 // le mode commun. 286 type RecuFiscal struct { 287 dm.RecuFiscal 288 Donateur rd.Personne `json:"donateur,omitempty"` 289 } 290 291 // renvoie le numéro de reçu (unique par donateur / année) 292 func (r RecuFiscal) numero() string { 293 n := time.Now() 294 return fmt.Sprintf("%04d%02d", r.Donateur.Id, n.Year()%100) 295 } 296 297 // RempliPdf Crée un recu, à l'aide de pdftk, qui doit être installé. 298 func RempliPdf(params RecuFiscal) ([]byte, error) { 299 champNum := champPdf{id: correspondancesChamps.numero, valeur: params.numero()} 300 donateur := donateur{ 301 nom: string(params.Donateur.Nom), 302 prenom: string(params.Donateur.Prenom), 303 adresse: string(params.Donateur.Adresse), 304 codePostal: string(params.Donateur.CodePostal), 305 ville: string(params.Donateur.Ville), 306 } 307 fields := []champPdf{champNum} 308 fields = append(fields, champsACVE()...) 309 fields = append(fields, champsDonateur(donateur)...) 310 fields = append(fields, champsDon(params.RecuFiscal)...) 311 fields = append(fields, champsTypeDon()...) 312 fields = append(fields, champsDateEdition()...) 313 fdf := forgeFDF(fields) 314 cmd := exec.Command("pdftk", ModelePath, "fill_form", "-", "output", "-") 315 cmd.Stdin = strings.NewReader(fdf) 316 errOut := new(bytes.Buffer) 317 cmd.Stderr = errOut 318 pdfBytes, err := cmd.Output() 319 if err != nil { 320 return nil, fmt.Errorf("Création du reçu impossible : %s : %s", err, errOut.String()) 321 } 322 return pdfBytes, nil 323 }