github.com/benoitkugler/goacve@v0.0.0-20201217100549-151ce6e55dc8/server/core/datamodel/acces.go (about) 1 // Ce package ajoute de l'intelligence aux données: 2 // - résolutions des liens entre tables 3 // - fonctions de recherches 4 // - implémentation des interfaces 5 package datamodel 6 7 import ( 8 "bytes" 9 "fmt" 10 "html/template" 11 "sort" 12 "strings" 13 14 "github.com/benoitkugler/goACVE/logs" 15 16 "github.com/benoitkugler/goACVE/server/core/rawdata/composites" 17 18 rd "github.com/benoitkugler/goACVE/server/core/rawdata" 19 ) 20 21 const ( 22 CouleurAgeInvalide = rd.HexColor("#80ea0000") 23 CouleurAttenteReponse = rd.HexColor("#90e0d616") 24 CouleurListeAttente = rd.HexColor("#90FF610F") 25 CouleurAnniversaire = rd.HexColor("#826a1b") 26 CouleurPaiementInvalide = rd.HexColor("#90e0552a") 27 CouleurPaiementBordereau = rd.HexColor("#9062872b") 28 CouleurResponsable = rd.HexColor("#4a086d11") 29 ) 30 31 const ( 32 PubEte OptionExport = iota 33 PubHiver 34 EchoRocher 35 ) 36 37 const ( 38 MembresTriAlphabetique OptionExport = iota 39 MembresTriCotisation 40 ) 41 42 var ( 43 CouleurGarcon = rd.RGBA{R: 19, G: 139, B: 215, A: 50} 44 CouleurFille = rd.RGBA{R: 199, G: 10, B: 126, A: 50} 45 CouleurParticipantTmp = rd.RGBA{R: 230, G: 157, B: 51, A: 130} 46 47 headerFacture = []string{ 48 "Participant", 49 "Camp", 50 "Dates", 51 "Prix du camp", 52 "Aides extérieures", 53 "Sous-total", 54 } 55 56 templateRowFacture = template.Must(template.New("rowFacture").Parse(`<tr> 57 <td> 58 {{ .Personne.NomPrenom }} <br/> 59 (né{{ .Personne.Sexe.Accord }} le {{ .Personne.DateNaissance }} 60 </td> 61 <td> 62 {{ .Camp.Label }} <br/> 63 N° JS : {{ .Camp.NumeroJS }} 64 </td> 65 <td> 66 du {{ .Camp.DateDebut }} <br /> 67 au {{ .Camp.DateFin }} 68 </td> 69 <td> 70 {{ .BilanPrix.PrixBase }} {{ .BilanPrix.DescriptionPrix }} 71 </td> 72 <td> 73 {{ range .BilanPrix.Aides }} 74 {{- if .Valide -}} 75 {{ .Description }} <br/> 76 {{- else -}} 77 (<i>{{ .Description }}</i>) <br/> 78 {{- end }} 79 {{ end }} 80 </td> 81 <td> 82 {{ .BilanPrix.PrixSansRemises }} 83 </td> 84 </tr>`)) 85 ) 86 87 type dataTemplateFacture struct { 88 Personne rd.Personne 89 Camp rd.Camp 90 BilanPrix BilanPrixCamp 91 } 92 93 // -------------------------------------------------------------------------- 94 // -------------------------------Personne ---------------------------------- 95 // -------------------------------------------------------------------------- 96 97 type OptionExport int 98 99 type CacheContacts map[int64][]rd.Organisme // id personnes -> structures contacts 100 101 func (b *BaseLocale) ResoudOrganismes() CacheContacts { 102 out := CacheContacts{} 103 for _, orga := range b.Organismes { 104 if contact := orga.IdContact; contact.IsNotNil() { 105 out[contact.Int64] = append(out[contact.Int64], orga) 106 } 107 } 108 return out 109 } 110 111 func (ac AccesPersonne) GetDocuments(cache LiensPersonneDocuments) []AccesDocumentPersonne { 112 if cache == nil { 113 cache = ac.Base.ResoudDocumentsPersonnes() 114 } 115 ids := cache[ac.Id] 116 out := make([]AccesDocumentPersonne, len(ids)) 117 for i, id := range ids { 118 out[i] = AccesDocumentPersonne{ac.Base.NewDocument(id)} 119 } 120 return out 121 } 122 123 // HasDocument renvoie `true` si la personne possède un document 124 // dans la catégorie donnée. 125 func (ac AccesPersonne) HasDocument(cache LiensPersonneDocuments, categorie rd.BuiltinContrainte) bool { 126 for _, doc := range ac.GetDocuments(cache) { 127 if doc.GetContrainte().Builtin == categorie { 128 return true 129 } 130 } 131 return false 132 } 133 134 // IsSB renvoie `true` si la personne a un document dans la catégorie SB. 135 func (ac AccesPersonne) IsSB(cache LiensPersonneDocuments) rd.Bool { 136 return rd.Bool(ac.HasDocument(cache, rd.CSb)) 137 } 138 139 // -------------------------------------------------------------------------- 140 // ------------------------------- Camp ------------------------------------- 141 // -------------------------------------------------------------------------- 142 143 // StatsCamp décrit le nombre de personnes dans différentes catégories. 144 type StatsCamp struct { 145 Inscrits, Attente, Equipe, PlacesRestantes, PlacesReservees, Garcons, Filles int 146 } 147 148 func (ac AccesCamp) GetGroupes() []rd.Groupe { 149 var out []rd.Groupe 150 for _, groupe := range ac.Base.Groupes { 151 if groupe.IdCamp == ac.Id { 152 out = append(out, groupe) 153 } 154 } 155 return out 156 } 157 158 func (ac AccesCamp) GetDirecteurEquipier() (AccesEquipier, bool) { 159 dir, has := ac.getEquipeMap(nil).FindDirecteur() 160 return ac.Base.NewEquipier(dir.Id), has 161 } 162 163 func (ac AccesCamp) GetDirecteur() (AccesPersonne, bool) { 164 dir, ok := ac.GetDirecteurEquipier() 165 if ok { 166 return dir.GetPersonne(), true 167 } 168 return AccesPersonne{}, false 169 } 170 171 func (ac AccesCamp) hintsAttente(cache LiensCampParticipants, personne rd.Personne, isCurrentInscrit bool) rd.HintsAttente { 172 parts := ac.getParticipants(cache) 173 cp := composites.CampParticipants{Camp: ac.RawData(), Participants: parts} 174 return cp.HintsAttente(personne.BasePersonne, isCurrentInscrit, ac.Base.Personnes) 175 } 176 177 // HintsAttente renvoie le statut conseillé pour le "participant" donné par 178 // `idPersonne`. Pour un participant déjà créé, voir `AccesParticipant.HintsAttente` 179 func (ac AccesCamp) HintsAttente(idPersonne int64, isCurrrentInscrit bool) rd.HintsAttente { 180 personne := ac.Base.Personnes[idPersonne] 181 return ac.hintsAttente(nil, personne, isCurrrentInscrit) 182 } 183 184 // Remplissage renvoi le ratio en % de remplissage du camp 185 // Si `cache` est nil, parcourt la table des participants. 186 func (ac AccesCamp) Remplissage(cache1 LiensCampParticipants, cache2 LiensCampParticipantsimples) rd.Pourcent { 187 nbInscrits := ac.GetNbInscrits(cache1, cache2) 188 nbPlaces := ac.RawData().NbPlaces 189 var remp rd.Pourcent 190 if nbPlaces > 0 { 191 remp = rd.Pourcent(nbInscrits * 100 / int(nbPlaces)) 192 } 193 return remp 194 } 195 196 // GetNbInscrits renvoie le nombre de participant effectif (sans liste d'attente) 197 func (ac AccesCamp) GetNbInscrits(cache1 LiensCampParticipants, cache2 LiensCampParticipantsimples) int { 198 if ac.RawData().InscriptionSimple { 199 return len(ac.GetParticipantsimples(cache2)) 200 } 201 return len(ac.GetInscrits(cache1)) 202 } 203 204 func (ac AccesCamp) getParticipants(cache LiensCampParticipants) (parts []rd.Participant) { 205 if cache == nil { 206 _, cache = ac.Base.ResoudParticipants() 207 } 208 ids := cache[ac.Id] 209 parts = make([]rd.Participant, len(ids)) 210 for index, id := range ids { 211 parts[index] = ac.Base.Participants[id] 212 } 213 return parts 214 } 215 216 // Si `cache` est nil, parcourt la table des participants. 217 func (ac AccesCamp) GetInscrits(cache LiensCampParticipants) []AccesParticipant { 218 var liste []AccesParticipant 219 for _, part := range ac.getParticipants(cache) { 220 if part.ListeAttente.IsInscrit() { 221 liste = append(liste, ac.Base.NewParticipant(part.Id)) 222 } 223 } 224 return liste 225 } 226 227 // Si `cache` est nil, parcourt la table des participants. 228 func (ac AccesCamp) GetAttente(cache LiensCampParticipants) []AccesParticipant { 229 var liste []AccesParticipant 230 for _, part := range ac.getParticipants(cache) { 231 if !part.ListeAttente.IsInscrit() { 232 liste = append(liste, ac.Base.NewParticipant(part.Id)) 233 } 234 } 235 return liste 236 } 237 238 func (ac AccesCamp) GetParticipantsimples(cache LiensCampParticipantsimples) []AccesParticipantsimple { 239 if cache == nil { 240 cache = ac.Base.ResoudParticipantsimples() 241 } 242 ids := cache[ac.Id] 243 parts := make([]AccesParticipantsimple, len(ids)) 244 for index, id := range ids { 245 parts[index] = ac.Base.NewParticipantsimple(id) 246 } 247 return parts 248 } 249 250 func (ac AccesCamp) getEquipeMap(cache LiensCampEquipiers) rd.Equipiers { 251 if cache == nil { 252 cache = ac.Base.ResoudEquipiers() 253 } 254 ids := cache[ac.Id] 255 out := make(rd.Equipiers, len(ids)) 256 for _, id := range ids { 257 out[id] = ac.Base.Equipiers[id] 258 } 259 return out 260 } 261 262 // Si `cache` est nil, parcourt la table des equipier. 263 func (ac AccesCamp) GetEquipe(cache LiensCampEquipiers) []AccesEquipier { 264 var liste []AccesEquipier 265 for id := range ac.getEquipeMap(cache) { 266 liste = append(liste, ac.Base.NewEquipier(id)) 267 } 268 269 sort.Slice(liste, func(i, j int) bool { 270 return liste[i].Id < liste[j].Id 271 }) 272 sort.SliceStable(liste, func(i, j int) bool { 273 return liste[i].RawData().Roles.Sortable() < liste[j].RawData().Roles.Sortable() 274 }) 275 return liste 276 } 277 278 // TriParticipants trie `l` sur place par ordre alphabétique, et 279 // par groupe si demandé. 280 func TriParticipants(l []AccesParticipant, triGroupe bool) { 281 sort.Slice(l, func(i, j int) bool { 282 return l[i].GetPersonne().RawData().Prenom.Sortable() < l[j].GetPersonne().RawData().Prenom.Sortable() 283 }) 284 sort.SliceStable(l, func(i, j int) bool { 285 return l[i].GetPersonne().RawData().Nom.Sortable() < l[j].GetPersonne().RawData().Nom.Sortable() 286 }) 287 if triGroupe { 288 sort.SliceStable(l, func(i, j int) bool { 289 gi, _ := l[i].GetGroupe() 290 gj, _ := l[j].GetGroupe() 291 return gi.Id < gj.Id 292 }) 293 } 294 } 295 296 // TriParticipantsItems trie `l` sur place par ordre alphabétique, et 297 // par groupe si demandé. 298 func TriParticipantsItems(parts rd.Table, triGroupe bool) { 299 SortBy(parts, PersonnePrenom, false) 300 SortStableBy(parts, PersonneNom, false) 301 if triGroupe { 302 SortStableBy(parts, ParticipantAnimateur, false) 303 SortStableBy(parts, ParticipantGroupe, false) 304 } 305 } 306 307 // GetListes renvoi la liste des inscrits et la liste d'attente. 308 // Si `critereBus` est non nul, ne renvoi que les participants matchant ce critère. 309 // Pour un camp simple, ces critères sont ignorés. 310 func (ac AccesCamp) GetListes(triGroupe bool, critereBus rd.Bus, hideNonValidated, selectPartageOK bool) (inscrits rd.Table, attente rd.Table) { 311 if ac.RawData().InscriptionSimple { 312 l := ac.GetParticipantsimples(nil) 313 inscrits = make(rd.Table, len(l)) 314 for i, part := range l { 315 inscrits[i] = part.AsItem() 316 } 317 } else { 318 _, cache := ac.Base.ResoudParticipants() 319 insc, att := ac.GetInscrits(cache), ac.GetAttente(cache) 320 inscrits, attente = make(rd.Table, 0, len(insc)), make(rd.Table, 0, len(att)) 321 for _, part := range insc { 322 if hideNonValidated { // ignore les participants en attente de validation 323 if f, ok := part.GetFacture(); ok && !f.RawData().IsValidated { 324 continue 325 } 326 } 327 if selectPartageOK { 328 // si un participant est lié à un dossier qui n'autorise pas 329 // le partage, ignore 330 if f, ok := part.GetFacture(); ok && !f.RawData().PartageAdressesOK { 331 continue 332 } 333 } 334 if part.RawData().Options.Bus.Match(critereBus) { 335 inscrits = append(inscrits, part.AsItem(cache)) 336 } 337 } 338 for _, part := range att { 339 if hideNonValidated { // ignore les participants en attente de validation 340 if f, ok := part.GetFacture(); ok && !f.RawData().IsValidated { 341 continue 342 } 343 } 344 if part.RawData().Options.Bus.Match(critereBus) { 345 attente = append(attente, part.AsItem(cache)) 346 } 347 } 348 } 349 TriParticipantsItems(inscrits, triGroupe) 350 TriParticipantsItems(attente, triGroupe) 351 return inscrits, attente 352 } 353 354 // StatsCamp renvoie un détails du nombre de personnes par catégorie. 355 func (ac AccesCamp) GetStats(cacheP LiensCampParticipants, cachePs LiensCampParticipantsimples, cacheE LiensCampEquipiers) StatsCamp { 356 inscrits, inscritssimples := ac.GetInscrits(cacheP), ac.GetParticipantsimples(cachePs) 357 nbInscrits, nbG, nbF := len(inscrits)+len(inscritssimples), 0, 0 358 for _, part := range inscrits { 359 switch part.GetPersonne().RawData().Sexe { 360 case "M": 361 nbG += 1 362 case "F": 363 nbF += 1 364 } 365 } 366 r := ac.RawData() 367 return StatsCamp{ 368 Inscrits: nbInscrits, 369 Attente: len(ac.GetAttente(cacheP)), 370 Equipe: len(ac.GetEquipe(cacheE)), 371 PlacesRestantes: int(r.NbPlaces) - nbInscrits, 372 PlacesReservees: int(r.NbPlacesReservees), 373 Garcons: nbG, 374 Filles: nbF, 375 } 376 } 377 378 // TrouveGroupe cherche parmis les groupes possibles celui qui pourrait convenir. 379 // Normalement, les groupes respectent un invariant de continuité sur les plages, 380 // imposé par le frontend. 381 // Si plusieurs peuvent convenir, un seul est renvoyé, de façon arbitraire. 382 func (ac AccesCamp) TrouveGroupe(dateNaissance rd.Date) (rd.Groupe, bool) { 383 cg := composites.CampGroupes{Camp: ac.RawData(), Groupes: ac.GetGroupes()} 384 return cg.TrouveGroupe(dateNaissance) 385 } 386 387 // GetRegisteredDocuments renvoie les documents (enregistrés sur la base) 388 // liés au camp. 389 // Si `withLettre` vaut false, seules les pièces jointes additionnelles sont renvoyés. 390 // Si `withPiecesJointes` vaut false, seules les lettres sont renvoyés. 391 func (ac AccesCamp) GetRegisteredDocuments(withLettre, withPiecesJointes bool) (out []rd.Document) { 392 for _, lien := range ac.Base.DocumentCamps { 393 if lien.IdCamp == ac.Id { 394 doc := ac.Base.Documents[lien.IdDocument] 395 if (withLettre && lien.IsLettre) || (withPiecesJointes && !lien.IsLettre) { 396 out = append(out, doc) 397 } 398 } 399 } 400 return out 401 } 402 403 func (ac AccesGroupe) GetCamp() AccesCamp { 404 return ac.Base.NewCamp(ac.RawData().IdCamp) 405 } 406 407 func (ac AccesGroupe) AsGroupeCamp() composites.GroupeCamp { 408 return composites.GroupeCamp{ 409 Groupe: ac.RawData(), 410 Camp: ac.GetCamp().RawData(), 411 } 412 } 413 414 // Renvoie les ids des contraintes imposées au groupe. 415 func (ac AccesGroupe) GetContraintes() rd.GroupeContraintes { 416 return ac.Base.GroupeContraintes[ac.Id] 417 } 418 419 // Label inclut le nom du camp 420 func (ac AccesGroupe) Label() rd.String { 421 camp := ac.GetCamp() 422 out := camp.RawData().Label() 423 if len(camp.GetGroupes()) >= 1 { 424 out += " - " + ac.RawData().Nom 425 } 426 return out 427 } 428 429 // -------------------------------------------------------------------------- 430 // ------------------------------- Participant ------------------------------ 431 // -------------------------------------------------------------------------- 432 433 func (ac AccesParticipant) AsParticipantCamp() composites.ParticipantCamp { 434 return composites.ParticipantCamp{ 435 Participant: ac.RawData(), 436 Camp: ac.GetCamp().RawData(), 437 } 438 } 439 440 func (ac AccesParticipant) AsParticipantPersonne() composites.ParticipantPersonne { 441 return composites.ParticipantPersonne{ 442 Participant: ac.RawData(), 443 Personne: ac.GetPersonne().RawData(), 444 } 445 } 446 447 func (ac AccesParticipant) GetCamp() AccesCamp { 448 return ac.Base.NewCamp(ac.RawData().IdCamp) 449 } 450 451 func (ac AccesParticipant) GetPersonne() AccesPersonne { 452 return ac.Base.NewPersonne(ac.RawData().IdPersonne) 453 } 454 455 func (ac AccesParticipant) GetFacture() (AccesFacture, bool) { 456 idFac := ac.RawData().IdFacture 457 if !idFac.Valid { 458 return AccesFacture{}, false 459 } 460 return ac.Base.NewFacture(idFac.Int64), true 461 } 462 463 func (ac AccesParticipant) GetGroupe() (rd.Groupe, bool) { 464 lien, has := ac.Base.ParticipantGroupe[ac.Id] 465 if has { 466 return ac.Base.NewGroupe(lien.IdGroupe).RawData(), true 467 } 468 return rd.Groupe{}, false 469 } 470 471 func (ac AccesParticipant) GetAnimateur() (composites.EquipierPersonne, bool) { 472 lien, has := ac.Base.ParticipantEquipier[ac.Id] 473 if has { 474 eq := ac.Base.Equipiers[lien.IdEquipier] 475 pers := ac.Base.Personnes[eq.IdPersonne] 476 return composites.EquipierPersonne{Equipier: eq, Personne: pers}, true 477 } 478 return composites.EquipierPersonne{}, false 479 } 480 481 // HintsAttente évalue si le participant devrait être placé 482 // en liste d'attente ou non. 483 // Certains critères ne sont pertinents que pour un participant 484 // actuellement en liste d'attente. 485 func (ac AccesParticipant) HintsAttente(cache rd.LiensCampParticipants) rd.HintsAttente { 486 personne := ac.GetPersonne().RawData() 487 isAlreadyInscrit := ac.RawData().ListeAttente.Statut == rd.Inscrit 488 return ac.GetCamp().hintsAttente(cache, personne, isAlreadyInscrit) 489 } 490 491 func (ac AccesParticipant) IsAgeValide() bool { 492 statusMin, statusMax := ac.GetCamp().RawData().HasAgeValide(ac.GetPersonne().RawData().BasePersonne) 493 return statusMin == rd.Inscrit && statusMax == rd.Inscrit 494 } 495 496 // GetAides renvoies les aides liées au participant. 497 // Si `cache` vaut nil, parcourt la table des aides. 498 func (ac AccesParticipant) GetAides(cache LiensParticipantAides) []AccesAide { 499 if cache == nil { 500 cache = ac.Base.ResoudAides() 501 } 502 ids := cache[ac.Id] 503 out := make([]AccesAide, len(ids)) 504 for index, id := range ids { 505 out[index] = ac.Base.NewAide(id) 506 } 507 return out 508 } 509 510 // StatutPrix renvoi la catégorie de prix du participant 511 // Une catégorie avec un Id vide équivaut à l'absence de catégorie 512 func (ac AccesParticipant) StatutPrix() rd.PrixParStatut { 513 s := ac.RawData().OptionPrix.Statut 514 if s == 0 { 515 return rd.PrixParStatut{} 516 } 517 campOption := ac.GetCamp().RawData().OptionPrix 518 if campOption.Active != rd.OptionsPrix.STATUT { 519 return rd.PrixParStatut{} 520 } 521 for _, info := range campOption.Statut { 522 if info.Id == s { 523 return info 524 } 525 } 526 return rd.PrixParStatut{} 527 } 528 529 type BilanPrixCamp = composites.BilanPrixCamp 530 531 // EtatFinancier calcule le prix du sejour pour le participant, 532 // en prenant en compte les options, les remises et les aides. 533 // Si `withAidesInvalides` vaut `false`, les aides non validées ne sont pas renvoyées. 534 func (ac AccesParticipant) EtatFinancier(cache LiensParticipantAides, withAidesInvalides bool) BilanPrixCamp { 535 // résolution des données 536 fac, _ := ac.GetFacture() 537 cp := composites.ParticipantComplet{ 538 ParticipantCamp: ac.AsParticipantCamp(), 539 // zero value si le participant n'a pas de facture 540 FacturePersonne: fac.AsFacturePersonne(), 541 } 542 // résolutions des aides 543 var aides []composites.AideStructureaide 544 for _, aide := range ac.GetAides(cache) { 545 aides = append(aides, aide.WithStructure()) 546 } 547 return cp.EtatFinancier(aides, withAidesInvalides) 548 } 549 550 func (ac AccesParticipant) IsFicheSanitaireUpToDate() rd.OptionnalBool { 551 return ac.AsParticipantPersonne().IsFicheSanitaireUpToDate() 552 } 553 554 func (ac AccesParticipant) GetPresence() rd.Plage { 555 plage, _ := ac.AsParticipantCamp().AsOptionParticipantCamp().Presence() 556 return plage 557 } 558 559 // ------------------------------------------------------------------------- 560 561 func (ac AccesParticipantsimple) GetCamp() AccesCamp { 562 return ac.Base.NewCamp(ac.RawData().IdCamp) 563 } 564 565 func (ac AccesParticipantsimple) GetPersonne() AccesPersonne { 566 return ac.Base.NewPersonne(ac.RawData().IdPersonne) 567 } 568 569 // GetPresence renvoie la date d'arrivée et départ du participant 570 func (ac AccesParticipantsimple) GetPresence() rd.Plage { 571 camp := ac.GetCamp().RawData() 572 arrivee, depart := camp.DateDebut, camp.DateFin 573 return rd.Plage{From: arrivee, To: depart} 574 } 575 576 // AsItemChilds renvoie une stucture hierachique sans enfants 577 func (ac AccesParticipantsimple) AsItemChilds() rd.ItemChilds { 578 item := ac.AsItem() 579 return rd.ItemChilds{Item: item} 580 } 581 582 // -------------------------------------------------------------------------- 583 // ----------------------------- Equipier ----------------------------------- 584 // -------------------------------------------------------------------------- 585 586 func (ac AccesEquipier) GetPersonne() AccesPersonne { 587 return ac.Base.NewPersonne(ac.RawData().IdPersonne) 588 } 589 590 func (ac AccesEquipier) GetCamp() AccesCamp { 591 return ac.Base.NewCamp(ac.RawData().IdCamp) 592 } 593 594 func (ac AccesEquipier) GetContraintes() rd.EquipierContraintes { 595 return ac.Base.EquipierContraintes[ac.Id] 596 } 597 598 func safeChange(date, candidat rd.Date) rd.Date { 599 if !candidat.Time().IsZero() { 600 return candidat 601 } 602 return date 603 } 604 605 // GetPresence renvoie la date d'arrivée et départ du participant 606 // Elles sont déterminés par le champ `Présence` 607 // (puis par défaut les dates du camp) 608 func (ac AccesEquipier) GetPresence() rd.Plage { 609 camp := ac.GetCamp() 610 rawCamp := camp.RawData() 611 612 // plus faible priorité 613 arrivee, depart := rawCamp.DateDebut, rawCamp.DateFin 614 615 rawPart := ac.RawData() 616 if rawPart.Presence.Active { 617 arrivee, depart = safeChange(arrivee, rawPart.Presence.From), safeChange(depart, rawPart.Presence.To) 618 } 619 return rd.Plage{From: arrivee, To: depart} 620 } 621 622 // -------------------------------------------------------------------------- 623 // ------------------------------- Aide ------------------------------------- 624 // -------------------------------------------------------------------------- 625 626 func (ac AccesAide) GetStructureaide() AccesStructureaide { 627 return ac.Base.NewStructureaide(ac.RawData().IdStructureaide) 628 } 629 630 func (ac AccesAide) GetParticipant() AccesParticipant { 631 return ac.Base.NewParticipant(ac.RawData().IdParticipant) 632 } 633 634 func (ac AccesAide) AsAideParticipantCamp() composites.AideParticipantCamp { 635 return composites.AideParticipantCamp{ 636 Aide: ac.RawData(), 637 ParticipantCamp: ac.GetParticipant().AsParticipantCamp(), 638 } 639 } 640 641 // ValeurEffective renvoie : 642 // - le montant dans le cas d'une aide absolue 643 // - le montant fois le nombre de jours (en prenant en compte une éventuelle limite) sinon 644 // Si `withRemise` vaut true les remises (%) sont appliquées. 645 func (ac AccesAide) ValeurEffective(withRemise bool) rd.Euros { 646 return ac.AsAideParticipantCamp().ValeurEffective(withRemise) 647 } 648 649 // GetDocuments renvoie la liste des pièces justificatives associées à l'aide. 650 func (ac AccesAide) GetDocuments() rd.Table { 651 aideToDocs := ac.Base.ResoudDocumentsAides() 652 ids := aideToDocs[ac.Id] 653 out := make(rd.Table, len(ids)) 654 for i, id := range ids { 655 out[i] = AccesDocumentAide{ac.Base.NewDocument(id)}.AsItem() 656 } 657 return out 658 } 659 660 func (ac AccesAide) Label() string { 661 return ac.GetParticipant().GetPersonne().RawData().NomPrenom().String() + " - " + string(ac.GetStructureaide().RawData().Nom) 662 } 663 664 func (ac AccesAide) Description() string { 665 return fmt.Sprintf("%s : %s", ac.GetStructureaide().RawData().Nom, ac.ValeurEffective(false)) 666 } 667 668 func (ac AccesAide) WithStructure() composites.AideStructureaide { 669 return composites.AideStructureaide{ 670 Aide: ac.RawData(), 671 Structureaide: ac.GetStructureaide().RawData(), 672 } 673 } 674 675 // -------------------------------------------------------------------------- 676 // -------------------------------- Facture --------------------------------- 677 // -------------------------------------------------------------------------- 678 679 func (ac AccesFacture) GetPersonne() AccesPersonne { 680 return ac.Base.NewPersonne(ac.RawData().IdPersonne) 681 } 682 683 func (ac AccesFacture) AsFacturePersonne() composites.FacturePersonne { 684 return composites.FacturePersonne{ 685 Facture: ac.RawData(), 686 Personne: ac.GetPersonne().RawData(), 687 } 688 } 689 690 // GetDossiers renvoie les dossiers associés à la facture. 691 // Si l'attribut `cache` vaut nil, 692 // effectue un parcourt de la table des participants. 693 func (ac AccesFacture) GetDossiers(cache LiensFactureParticipants) []AccesParticipant { 694 if cache == nil { 695 cache, _ = ac.Base.ResoudParticipants() 696 } 697 ids := cache[ac.Id] 698 out := make([]AccesParticipant, len(ids)) 699 for index, id := range ids { 700 out[index] = ac.Base.NewParticipant(id) 701 } 702 sort.Slice(out, func(i, j int) bool { 703 return out[i].Id < out[j].Id 704 }) 705 return out 706 } 707 708 func (ac AccesFacture) AsItemChilds(cache1 LiensFactureParticipants, cache2 LiensFactureMessages, cache3 LiensFacturePaiements, cache4 LiensCampParticipants) rd.ItemChilds { 709 item := ac.AsItem(cache1, cache2, cache3) 710 doss := ac.GetDossiers(cache1) 711 childs := make(rd.Table, len(doss)) 712 for index, d := range doss { 713 childs[index] = d.AsItem(cache4) 714 } 715 return rd.ItemChilds{Item: item, Childs: childs} 716 } 717 718 // GetPaiements renvoies les paiements liés à la facture. 719 // Si l'attribut `cache` vaut nil, 720 // effectue un parcourt de la table des paiements 721 func (ac AccesFacture) GetPaiements(cache LiensFacturePaiements, withInvalides bool) []AccesPaiement { 722 if cache == nil { 723 cache = ac.Base.ResoudPaiements() 724 } 725 ids := cache[ac.Id] 726 out := make([]AccesPaiement, 0, len(ids)) 727 for _, Id := range ids { 728 ac := ac.Base.NewPaiement(Id) 729 if withInvalides || bool(!ac.RawData().IsInvalide) { 730 out = append(out, ac) 731 } 732 } 733 return out 734 } 735 736 // ChoixDestinataires renvoie les destinataires possibles, y compris 737 // le responsable, qui est garantit d'être en première position 738 func (ac AccesFacture) ChoixDestinataires() rd.DestinatairesOptionnels { 739 d := ac.GetPersonne().RawData().ToDestinataire() 740 others := ac.RawData().DestinatairesOptionnels 741 return append(rd.DestinatairesOptionnels{d}, others...) 742 } 743 744 // BilanFacture résume l'état d'une facture. 745 // (il peut être utilisé pour éviter de répéter les mêmes calculs ) 746 type BilanFacture struct { 747 Paiements []rd.Paiement 748 Participants []AccesParticipant 749 Demande, Recu rd.Euros 750 } 751 752 func (b BilanFacture) IsAcquitte() bool { 753 return b.Demande.IsLess(b.Recu) 754 } 755 756 func (b BilanFacture) IsExactAcquitte() bool { 757 return b.Demande.IsEqual(b.Recu) 758 } 759 760 func (b BilanFacture) ApresPaiement() rd.Euros { 761 return b.Demande - b.Recu 762 } 763 764 func (b BilanFacture) StatutPaiement() rd.Completion { 765 if b.Participants == nil { 766 return rd.Invalide 767 } 768 if b.IsAcquitte() { 769 return rd.Complete 770 } else if b.Recu > 0 { 771 return rd.EnCours 772 } else { 773 return rd.NonCommencee 774 } 775 } 776 777 type CacheEtatFinancier struct { 778 PAides LiensParticipantAides 779 FPaiements LiensFacturePaiements 780 FParticipants LiensFactureParticipants 781 } 782 783 // NewCacheEtatFinancier parcourts les tables des aides, paiements et participants 784 func (b *BaseLocale) NewCacheEtatFinancier() CacheEtatFinancier { 785 var cache CacheEtatFinancier 786 cache.PAides = b.ResoudAides() 787 cache.FPaiements = b.ResoudPaiements() 788 cache.FParticipants, _ = b.ResoudParticipants() 789 return cache 790 } 791 792 // EtatFinancier additionne le prix pour chaque participant et 793 // décompte les paiements effectués. Seuls les participants inscrits (en liste principale) 794 // sont pris en compte. 795 // Les participants en liste d'attente sont ignorés. 796 // `withInvalides` contrôle si les aides et paiements invalides sont affichés ou non. 797 // Les maps de `cache` sont générés si `nil` 798 func (ac AccesFacture) EtatFinancier(cache CacheEtatFinancier, withInvalides bool) BilanFacture { 799 var out BilanFacture 800 801 if cache.PAides == nil { 802 cache.PAides = ac.Base.ResoudAides() 803 } 804 805 for _, participant := range ac.GetDossiers(cache.FParticipants) { 806 if !participant.RawData().ListeAttente.IsInscrit() { 807 continue 808 } 809 prixParticipant := participant.EtatFinancier(cache.PAides, withInvalides) // 810 out.Demande += prixParticipant.PrixNet() 811 out.Participants = append(out.Participants, participant) 812 } 813 814 for _, paiement := range ac.GetPaiements(cache.FPaiements, false) { 815 raw := paiement.RawData() 816 if raw.IsRemboursement { 817 out.Recu -= raw.Valeur 818 } else { 819 out.Recu += raw.Valeur 820 } 821 out.Paiements = append(out.Paiements, raw) 822 } 823 return out 824 } 825 826 // Camps renvois les camps concernés par la facture, ainsi qu'une description 827 func (ac AccesFacture) Camps(cache LiensFactureParticipants) (map[int64]bool, string) { 828 ids, tmp := make(map[int64]bool), rd.StringSet{} 829 for _, doss := range ac.GetDossiers(cache) { 830 camp := doss.GetCamp().RawData() 831 ids[camp.Id] = true 832 tmp[camp.Label().String()] = true 833 } 834 noms := tmp.ToList() 835 sort.Slice(noms, func(i, j int) bool { 836 return noms[i] > noms[j] 837 }) 838 return ids, strings.Join(noms, ", ") 839 } 840 841 // participants renvoit les participants 842 func (ac AccesFacture) participants(cache LiensFactureParticipants) string { 843 idsPers := rd.NewSet() 844 for _, doss := range ac.GetDossiers(cache) { 845 idsPers.Add(doss.GetPersonne().Id) 846 } 847 var chunks []string 848 for id := range idsPers { 849 chunks = append(chunks, ac.Base.NewPersonne(id).RawData().NomPrenom().String()) 850 } 851 sort.Strings(chunks) // déterminisme 852 return strings.Join(chunks, ", ") 853 } 854 855 // Description renvoie un tableau Html résumant les participants et les 856 // paiements associés à la facture. 857 // Les paiements et aides invalides sont affichés. 858 func (ac AccesFacture) Description() string { 859 bilan := ac.EtatFinancier(CacheEtatFinancier{}, true) 860 s := "<table cellpadding='5'><tr>" 861 for _, header := range headerFacture { 862 s += "<th>" + header + "</th>" 863 } 864 s += "</tr>" 865 sepTab := " " 866 for _, dossier := range bilan.Participants { 867 var row bytes.Buffer 868 bilanPrix := dossier.EtatFinancier(nil, true) 869 err := templateRowFacture.Execute(&row, dataTemplateFacture{ 870 Personne: dossier.GetPersonne().RawData(), 871 Camp: dossier.GetCamp().RawData(), 872 BilanPrix: bilanPrix, 873 }) 874 if err == nil { 875 s += row.String() 876 } else { 877 s += "<tr>" + err.Error() + "</tr>" 878 } 879 rem := bilanPrix.Remises.Description(sepTab) 880 sousTotal := bilanPrix.PrixNet().String() 881 if len(rem) > 0 { 882 s += fmt.Sprintf(`<tr><td colspan="%d" style="text-align: right;">%s</td><td color="blue">%s</td></tr>`, 883 len(headerFacture)-1, strings.Join(rem, sepTab), sousTotal) 884 } 885 } 886 s += fmt.Sprintf(`<tr><td colspan=%d style="text-align: right;">%s</td><td><i>%s</i></td></tr>`, 887 len(headerFacture)-1, "Total (après remises)", bilan.Demande) 888 for _, paiement := range ac.GetPaiements(nil, true) { 889 desc, montant := paiement.RawData().Description() 890 s += fmt.Sprintf(`<tr><td colspan="2"><td colspan="%d" style="text-align: left;"><a href='%d'>%s</a></td> 891 <td>%s</td></tr>`, 892 len(headerFacture)-3, paiement.Id, desc, montant) 893 } 894 s += fmt.Sprintf(`<tr><td colspan=%d style="text-align: right;">%s</td><td><b>%s</b></td></tr>`, 895 len(headerFacture)-1, "Total (après paiements)", bilan.ApresPaiement()) 896 s += "</table>" 897 return s 898 } 899 900 // LabelVirement renvoie le label (préfixé) permettant d'identifier le dossier 901 // lors d'un virement. 902 func (ac AccesFacture) LabelVirement() string { 903 return logs.LabelVirement.Offusc(ac.Id) 904 } 905 906 // NeedAcompte indique si un acompte est demandé sur l'un des camps du dossier. 907 func (ac AccesFacture) NeedAcompte() bool { 908 for _, part := range ac.GetDossiers(nil) { 909 if part.GetCamp().RawData().SchemaPaiement == rd.SPAcompte { 910 return true 911 } 912 } 913 return false 914 } 915 916 // -------------------------------------------------------------------------- 917 // ------------------------------ Paiement ---------------------------------- 918 // -------------------------------------------------------------------------- 919 func (ac AccesPaiement) GetFacture() AccesFacture { 920 return ac.Base.NewFacture(ac.RawData().IdFacture) 921 } 922 923 // -------------------------------------------------------------------------- 924 // ------------------------------ Document ---------------------------------- 925 // -------------------------------------------------------------------------- 926 927 type AccesDocumentPersonne struct { 928 AccesDocument 929 } 930 931 func (b *BaseLocale) NewDocumentPersonne(id int64) AccesDocumentPersonne { 932 return AccesDocumentPersonne{b.NewDocument(id)} 933 } 934 935 // GetContrainte renvoie la contrainte pour un document 936 // attaché à une personne 937 func (ac AccesDocumentPersonne) GetContrainte() rd.Contrainte { 938 lien := ac.Base.DocumentPersonnes[ac.Id] 939 return ac.Base.Contraintes[lien.IdContrainte] 940 } 941 942 func (ac AccesDocumentPersonne) GetProprietaire() AccesPersonne { 943 lien := ac.Base.DocumentPersonnes[ac.Id] 944 return ac.Base.NewPersonne(lien.IdPersonne) 945 } 946 947 type AccesDocumentAide struct { 948 AccesDocument 949 } 950 951 func (ac AccesDocumentAide) GetAide() AccesAide { 952 lien := ac.Base.DocumentAides[ac.Id] 953 return ac.Base.NewAide(lien.IdAide) 954 } 955 956 // -------------------------------------------------------------------------- 957 // --------------------------------- Don ------------------------------------ 958 // -------------------------------------------------------------------------- 959 960 // PersonneOrganisme regroupe les éléments affichés 961 // dans la liste des personnes : personne ou organisme 962 type PersonneOrganisme interface { 963 // CoordonneesDons renvoie les coordonnées utilisées 964 // pour un don. 965 CoordonneesDons() rd.Coordonnees 966 } 967 968 // CoordonneesDons renvoie les coordonnées de la personne 969 func (ac AccesPersonne) CoordonneesDons() rd.Coordonnees { 970 return ac.RawData().Coordonnees() 971 } 972 973 // CoordonneesDons choisit en priorité le contact spécifique aux dons, 974 // puis le contact normal (propre ou non). 975 func (ac AccesOrganisme) CoordonneesDons() rd.Coordonnees { 976 raw := ac.RawData() 977 if cd := raw.IdContactDon; cd.IsNotNil() { 978 return ac.Base.Personnes[cd.Int64].Coordonnees() 979 } else if c := raw.IdContact; c.IsNotNil() { 980 return ac.Base.Personnes[c.Int64].Coordonnees() 981 } else { 982 return raw.Contact 983 } 984 } 985 986 // GetDonateur renvoie le donateur, possiblement nil 987 func (ac AccesDon) GetDonateur() PersonneOrganisme { 988 donateur := ac.Base.DonDonateurs[ac.Id] 989 switch id := donateur.IId().(type) { 990 case rd.IdPersonne: 991 return ac.Base.NewPersonne(id.Int64()) 992 case rd.IdOrganisme: 993 return ac.Base.NewOrganisme(id.Int64()) 994 default: 995 return nil 996 } 997 }