Premier commit

This commit is contained in:
2026-09-11 14:55:41 +02:00
commit 8ed04aae89
134 changed files with 36627 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
namespace SIFinance.Shared;
// Section "AzureAd" de appsettings*.json — App registration Azure AD "SI Finance",
// utilisée à la fois pour le flux OAuth2 (authorization code + PKCE, connexion
// utilisateur) dans EntraAuthEndpoints et pour l'appel Graph "app-only" (client
// credentials) dans EntraSyncService/EntraGraphTokenProvider. ClientSecret et GroupId
// sont renseignés dans appsettings.Development.json (secrets, non commités).
public class AzureAdOptions
{
public const string SectionName = "AzureAd";
public string TenantId { get; set; } = "";
public string ClientId { get; set; } = "";
public string ClientSecret { get; set; } = "";
public string RedirectUri { get; set; } = "";
public string GroupId { get; set; } = "";
}
+38
View File
@@ -0,0 +1,38 @@
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using SIFinance.Shared.Entities;
namespace SIFinance.Shared;
public class CurrentUserService(IHttpContextAccessor httpContextAccessor)
{
public Task<int> GetOrCreateCurrentUserIdAsync(CancellationToken ct = default)
{
var principal = httpContextAccessor.HttpContext?.User
?? throw new InvalidOperationException("Aucun contexte HTTP disponible pour résoudre l'utilisateur courant.");
var idClaim = principal.FindFirst("id")?.Value
?? throw new UnauthorizedAccessException("Le jeton ne contient pas de claim 'id' — utilisateur non identifiable.");
return Task.FromResult(int.Parse(idClaim));
}
// Vérifie si l'utilisateur courant possède le rôle donné (ex. "FINANCE").
// Passe par une jointure utilisateurs <-> utilisateur_roles via azure_oid, car
// le JWT ne porte que l'Id interne, pas l'azure_oid.
public async Task<bool> HasRoleAsync(string role, SIFinanceDbContext db, CancellationToken ct = default)
{
var userId = await GetOrCreateCurrentUserIdAsync(ct);
// Les habilitations fonctionnelles viennent exclusivement de
// core.utilisateur_roles. Un profil Entra ou le statut Responsable ne donne
// jamais à lui seul l'accès aux écrans ou actions Finance.
return await db.UtilisateurRoles
.AsNoTracking()
.Join(db.Utilisateurs.AsNoTracking(),
ur => ur.AzureOid,
u => u.AzureOid,
(ur, u) => new { u.Id, ur.Role })
.AnyAsync(x => x.Id == userId && x.Role == role, ct);
}
}
+33
View File
@@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
namespace SIFinance.Shared.Entities;
public partial class Acompte
{
public int Id { get; set; }
public int FactureId { get; set; }
public decimal Montant { get; set; }
public DateOnly DatePrevue { get; set; }
public DateOnly? DateVerse { get; set; }
public string Statut { get; set; } = null!;
public int? JustificatifId { get; set; }
public string? Commentaire { get; set; }
public int SaisieParId { get; set; }
public DateTime? DateCreation { get; set; }
public virtual Facture Facture { get; set; } = null!;
public virtual PiecesJointe? Justificatif { get; set; }
public virtual Utilisateur SaisiePar { get; set; } = null!;
}
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
namespace SIFinance.Shared.Entities;
public partial class AffectationsAvoir
{
public int Id { get; set; }
public int AvoirId { get; set; }
public int FactureId { get; set; }
public decimal MontantAffecte { get; set; }
public int AffecteParId { get; set; }
public DateTime? DateAffectation { get; set; }
public virtual Utilisateur AffectePar { get; set; } = null!;
public virtual Avoir Avoir { get; set; } = null!;
public virtual Facture Facture { get; set; } = null!;
}
+25
View File
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
namespace SIFinance.Shared.Entities;
public partial class Audit
{
public long Id { get; set; }
public string EntiteType { get; set; } = null!;
public int EntiteId { get; set; }
public int UtilisateurId { get; set; }
public string Action { get; set; } = null!;
public string? DetailJson { get; set; }
public string? IpAdresse { get; set; }
public DateTime DateAction { get; set; }
public virtual Utilisateur Utilisateur { get; set; } = null!;
}
+37
View File
@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
namespace SIFinance.Shared.Entities;
public partial class Avoir
{
public int Id { get; set; }
public int FournisseurId { get; set; }
public string NumeroAvoir { get; set; } = null!;
public DateOnly DateAvoir { get; set; }
public decimal MontantInitial { get; set; }
public decimal SoldeDisponible { get; set; }
public int? FactureId { get; set; }
public int? JustificatifId { get; set; }
public int SaisieParId { get; set; }
public DateTime? DateCreation { get; set; }
public virtual ICollection<AffectationsAvoir> AffectationsAvoirs { get; set; } = new List<AffectationsAvoir>();
public virtual Facture? Facture { get; set; }
public virtual Fournisseur Fournisseur { get; set; } = null!;
public virtual PiecesJointe? Justificatif { get; set; }
public virtual Utilisateur SaisiePar { get; set; } = null!;
}
@@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
namespace SIFinance.Shared.Entities;
// Budget annuel d'un service pour le module Achats. Exercice non calendaire (1er
// septembre au 31 août) — ExerciceAnnee porte l'année de démarrage (ex. 2026 = exercice
// du 01/09/2026 au 31/08/2027). Le montant "actif" du budget est MontantInitial tant
// qu'aucune révision n'existe, sinon la révision la plus récente (cf. Revisions) —
// chaque révision est conservée pour l'historique, jamais écrasée.
public partial class BudgetService
{
public int Id { get; set; }
public int ServiceId { get; set; }
public int ExerciceAnnee { get; set; }
public decimal MontantInitial { get; set; }
public int SaisieParId { get; set; }
public DateTime? DateCreation { get; set; }
public DateTime? DateDerniereMaj { get; set; }
public virtual Service Service { get; set; } = null!;
public virtual Utilisateur SaisiePar { get; set; } = null!;
public virtual ICollection<BudgetServiceRevision> Revisions { get; set; } = new List<BudgetServiceRevision>();
}
@@ -0,0 +1,25 @@
using System;
namespace SIFinance.Shared.Entities;
// Historique des révisions d'un budget de service (augmentation en cours d'exercice,
// décidée par un responsable Finance) — une ligne par révision, jamais écrasée, pour
// garder la traçabilité complète de qui a changé le budget et pourquoi.
public partial class BudgetServiceRevision
{
public int Id { get; set; }
public int BudgetServiceId { get; set; }
public decimal MontantRevise { get; set; }
public string? Commentaire { get; set; }
public int RevisePar { get; set; }
public DateTime DateRevision { get; set; }
public virtual BudgetService BudgetService { get; set; } = null!;
public virtual Utilisateur ReviseParUtilisateur { get; set; } = null!;
}
+9
View File
@@ -0,0 +1,9 @@
namespace SIFinance.Shared.Entities;
public partial class Campus
{
public int Id { get; set; }
public string Libelle { get; set; } = null!;
public bool Actif { get; set; }
public virtual ICollection<ContratFournisseur> ContratsFournisseurs { get; set; } = new List<ContratFournisseur>();
}
@@ -0,0 +1,24 @@
using System;
namespace SIFinance.Shared.Entities;
public partial class ContratEcheance
{
public int Id { get; set; }
public int ContratId { get; set; }
public DateOnly DateEcheance { get; set; }
public decimal Montant { get; set; }
public string Statut { get; set; } = null!;
public int? JustificatifId { get; set; }
public DateTime? DateCreation { get; set; }
public virtual ContratFournisseur Contrat { get; set; } = null!;
public virtual PiecesJointe? Justificatif { get; set; }
}
@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
namespace SIFinance.Shared.Entities;
public partial class ContratFournisseur
{
public int Id { get; set; }
public int FournisseurId { get; set; }
public int? ServiceId { get; set; }
public int? CampusId { get; set; }
public string? Reference { get; set; }
public string Objet { get; set; } = null!;
public DateOnly? DateSignature { get; set; }
public DateOnly DateDebut { get; set; }
public DateOnly DateFin { get; set; }
public DateOnly? DateResiliation { get; set; }
public string? MotifResiliation { get; set; }
public decimal MontantTotal { get; set; }
public string Periodicite { get; set; } = null!;
public string Statut { get; set; } = null!;
public int SaisieParId { get; set; }
public DateTime? DateCreation { get; set; }
public DateTime? DateDerniereMaj { get; set; }
public virtual Fournisseur Fournisseur { get; set; } = null!;
public virtual Service? Service { get; set; }
public virtual Campus? Campus { get; set; }
public virtual Utilisateur SaisiePar { get; set; } = null!;
public virtual ICollection<ContratEcheance> Echeances { get; set; } = new List<ContratEcheance>();
public virtual ICollection<Facture> Factures { get; set; } = new List<Facture>();
public virtual ICollection<ContratFournisseurHistorique> Historique { get; set; } = new List<ContratFournisseurHistorique>();
}
@@ -0,0 +1,14 @@
namespace SIFinance.Shared.Entities;
public partial class ContratFournisseurHistorique
{
public int Id { get; set; }
public int ContratId { get; set; }
public string? AncienStatut { get; set; }
public string NouveauStatut { get; set; } = null!;
public string? Commentaire { get; set; }
public int UtilisateurId { get; set; }
public DateTime DateAction { get; set; }
public virtual ContratFournisseur Contrat { get; set; } = null!;
public virtual Utilisateur Utilisateur { get; set; } = null!;
}
@@ -0,0 +1,32 @@
using System;
namespace SIFinance.Shared.Entities;
public partial class ContratPrestataire
{
public int Id { get; set; }
public int OrganismeId { get; set; }
public string Reference { get; set; } = null!;
public string Objet { get; set; } = null!;
public DateOnly? DateSignature { get; set; }
public DateOnly DateDebut { get; set; }
public DateOnly DateFin { get; set; }
public decimal MontantTotal { get; set; }
public string Statut { get; set; } = null!;
public int SaisieParId { get; set; }
public DateTime? DateCreation { get; set; }
public virtual Organisme Organisme { get; set; } = null!;
public virtual Utilisateur SaisiePar { get; set; } = null!;
}
@@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
namespace SIFinance.Shared.Entities;
public partial class DemandesAchat
{
public int Id { get; set; }
public string Reference { get; set; } = null!;
public int ServiceId { get; set; }
public string Objet { get; set; } = null!;
public string? Description { get; set; }
public int? FournisseurId { get; set; }
public string? FournisseurLibre { get; set; }
public string? CategorieFournisseurLibre { get; set; }
public int TypeDepenseId { get; set; }
public decimal MontantHt { get; set; }
public decimal TauxTva { get; set; }
public decimal MontantTva { get; set; }
public decimal MontantTtc { get; set; }
public DateOnly DatePrevisionnelle { get; set; }
public int ModePaiementId { get; set; }
public string Statut { get; set; } = null!;
public int DemandeurId { get; set; }
public int? ValideurId { get; set; }
public decimal SeuilApplicable { get; set; }
public DateTime? DateCreation { get; set; }
public DateTime? DateSoumission { get; set; }
public DateTime? DateDerniereMaj { get; set; }
public virtual Utilisateur Demandeur { get; set; } = null!;
public virtual ICollection<Facture> Factures { get; set; } = new List<Facture>();
public virtual Fournisseur? Fournisseur { get; set; }
public virtual ModesPaiement ModePaiement { get; set; } = null!;
public virtual Service Service { get; set; } = null!;
public virtual TypesDepense TypeDepense { get; set; } = null!;
public virtual Utilisateur? Valideur { get; set; }
}
+77
View File
@@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
namespace SIFinance.Shared.Entities;
public partial class Facture
{
public int Id { get; set; }
public string SourceType { get; set; } = null!;
public int? DemandeAchatId { get; set; }
public int? ContratId { get; set; }
public int FournisseurId { get; set; }
public string NumeroFacture { get; set; } = null!;
public DateOnly DateFacture { get; set; }
public DateOnly DateEcheance { get; set; }
public decimal MontantHt { get; set; }
public decimal TauxTva { get; set; }
public decimal MontantTva { get; set; }
public decimal MontantTtc { get; set; }
public int? CategorieDepenseId { get; set; }
public int ServiceId { get; set; }
public int ModePaiementId { get; set; }
public string Statut { get; set; } = null!;
public bool? EstDoublonSignale { get; set; }
public string? Commentaire { get; set; }
public int SaisieParId { get; set; }
public int? ValideBapParId { get; set; }
public DateTime? DateValidationBap { get; set; }
public DateTime? DateCreation { get; set; }
public DateTime? DateDerniereMaj { get; set; }
public virtual ICollection<Acompte> Acomptes { get; set; } = new List<Acompte>();
public virtual ICollection<AffectationsAvoir> AffectationsAvoirs { get; set; } = new List<AffectationsAvoir>();
public virtual ICollection<Avoir> Avoirs { get; set; } = new List<Avoir>();
public virtual TypesDepense? CategorieDepense { get; set; }
public virtual DemandesAchat? DemandeAchat { get; set; }
public virtual ContratFournisseur? Contrat { get; set; }
public virtual Fournisseur Fournisseur { get; set; } = null!;
public virtual ModesPaiement ModePaiement { get; set; } = null!;
public virtual ICollection<Paiement> Paiements { get; set; } = new List<Paiement>();
public virtual Utilisateur SaisiePar { get; set; } = null!;
public virtual Service Service { get; set; } = null!;
public virtual Utilisateur? ValideBapPar { get; set; }
}
+25
View File
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
namespace SIFinance.Shared.Entities;
public partial class FichiersXml
{
public int Id { get; set; }
public string NomFichier { get; set; } = null!;
public string SharepointUrl { get; set; } = null!;
public DateTime? DateGeneration { get; set; }
public int GenereParId { get; set; }
public int NombreVirements { get; set; }
public decimal MontantTotal { get; set; }
public virtual Utilisateur GenerePar { get; set; } = null!;
public virtual ICollection<Paiement> Paiements { get; set; } = new List<Paiement>();
}
+37
View File
@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
namespace SIFinance.Shared.Entities;
public partial class Fournisseur
{
public int Id { get; set; }
public string RaisonSociale { get; set; } = null!;
public string? Siret { get; set; }
public string? Email { get; set; }
public string? Telephone { get; set; }
public string? Adresse { get; set; }
public byte[]? IbanChiffre { get; set; }
public string? Bic { get; set; }
public string? TitulaireCompte { get; set; }
public string Statut { get; set; } = null!;
public DateTime? DateCreation { get; set; }
public virtual ICollection<Avoir> Avoirs { get; set; } = new List<Avoir>();
public virtual ICollection<ContratFournisseur> ContratsFournisseurs { get; set; } = new List<ContratFournisseur>();
public virtual ICollection<DemandesAchat> DemandesAchats { get; set; } = new List<DemandesAchat>();
public virtual ICollection<Facture> Factures { get; set; } = new List<Facture>();
}
@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
namespace SIFinance.Shared.Entities;
public partial class HistoriqueAction
{
public int Id { get; set; }
public string EntiteType { get; set; } = null!;
public int EntiteId { get; set; }
public int UtilisateurId { get; set; }
public string Action { get; set; } = null!;
public string? Commentaire { get; set; }
public string? AncienStatut { get; set; }
public string? NouveauStatut { get; set; }
public DateTime? DateAction { get; set; }
public virtual Utilisateur Utilisateur { get; set; } = null!;
}
@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
namespace SIFinance.Shared.Entities;
public partial class ModesPaiement
{
public int Id { get; set; }
public string Libelle { get; set; } = null!;
public bool NecessiteXml { get; set; }
public bool Actif { get; set; }
public virtual ICollection<DemandesAchat> DemandesAchats { get; set; } = new List<DemandesAchat>();
public virtual ICollection<Facture> Factures { get; set; } = new List<Facture>();
public virtual ICollection<Paiement> Paiements { get; set; } = new List<Paiement>();
}
+25
View File
@@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
namespace SIFinance.Shared.Entities;
public partial class Organisme
{
public int Id { get; set; }
public string RaisonSociale { get; set; } = null!;
public string? Siret { get; set; }
public string? Email { get; set; }
public string? Telephone { get; set; }
public string? Adresse { get; set; }
public string Statut { get; set; } = null!;
public DateTime? DateCreation { get; set; }
public virtual ICollection<ContratPrestataire> ContratsPrestataires { get; set; } = new List<ContratPrestataire>();
}
+39
View File
@@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
namespace SIFinance.Shared.Entities;
public partial class Paiement
{
public int Id { get; set; }
public int FactureId { get; set; }
public decimal MontantPaye { get; set; }
public DateOnly DatePaiement { get; set; }
public int ModeReglementId { get; set; }
public string? ReferencePaiement { get; set; }
public int? FichierXmlId { get; set; }
public int EnregistreParId { get; set; }
public DateTime? DateSaisie { get; set; }
public string Statut { get; set; } = null!;
public bool Rapproche { get; set; }
public virtual Utilisateur EnregistrePar { get; set; } = null!;
public virtual Facture Facture { get; set; } = null!;
public virtual FichiersXml? FichierXml { get; set; }
public virtual ModesPaiement ModeReglement { get; set; } = null!;
public virtual ICollection<Rapprochement> Rapprochements { get; set; } = new List<Rapprochement>();
}
+19
View File
@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
namespace SIFinance.Shared.Entities;
public partial class Parametre
{
public string Cle { get; set; } = null!;
public string Valeur { get; set; } = null!;
public string? Description { get; set; }
public int? ModifiePar { get; set; }
public DateTime? DateModif { get; set; }
public virtual Utilisateur? ModifieParNavigation { get; set; }
}
+35
View File
@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
namespace SIFinance.Shared.Entities;
public partial class PiecesJointe
{
public int Id { get; set; }
public string EntiteType { get; set; } = null!;
public int EntiteId { get; set; }
public string TypePiece { get; set; } = null!;
public string NomFichier { get; set; } = null!;
public string SharepointUrl { get; set; } = null!;
public string SharepointItemId { get; set; } = null!;
public long TailleOctets { get; set; }
public int UploaderId { get; set; }
public DateTime? DateUpload { get; set; }
public virtual ICollection<Acompte> Acomptes { get; set; } = new List<Acompte>();
public virtual ICollection<Avoir> Avoirs { get; set; } = new List<Avoir>();
public virtual ICollection<ContratEcheance> ContratEcheances { get; set; } = new List<ContratEcheance>();
public virtual Utilisateur Uploader { get; set; } = null!;
}
@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
namespace SIFinance.Shared.Entities;
public partial class Rapprochement
{
public int Id { get; set; }
public int? PaiementId { get; set; }
public string? ReferenceBancaire { get; set; }
public DateOnly DateOperationBanq { get; set; }
public string? LibelleBanque { get; set; }
public decimal MontantBanque { get; set; }
public string Statut { get; set; } = null!;
public string? Commentaire { get; set; }
public int? TraiteParId { get; set; }
public DateTime? DateTraitement { get; set; }
public virtual Paiement? Paiement { get; set; }
public virtual Utilisateur? TraitePar { get; set; }
}
File diff suppressed because it is too large Load Diff
+19
View File
@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
namespace SIFinance.Shared.Entities;
public partial class Service
{
public int Id { get; set; }
public string Libelle { get; set; } = null!;
public bool Actif { get; set; }
public virtual ICollection<DemandesAchat> DemandesAchats { get; set; } = new List<DemandesAchat>();
public virtual ICollection<Facture> Factures { get; set; } = new List<Facture>();
public virtual ICollection<Utilisateur> Utilisateurs { get; set; } = new List<Utilisateur>();
}
+19
View File
@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
namespace SIFinance.Shared.Entities;
public partial class TypesDepense
{
public int Id { get; set; }
public string Libelle { get; set; } = null!;
public bool Actif { get; set; }
public bool EstMateriel { get; set; }
public virtual ICollection<DemandesAchat> DemandesAchats { get; set; } = new List<DemandesAchat>();
public virtual ICollection<Facture> Factures { get; set; } = new List<Facture>();
}
+70
View File
@@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
namespace SIFinance.Shared.Entities;
public partial class Utilisateur
{
public int Id { get; set; }
public string AzureOid { get; set; } = null!;
public string Nom { get; set; } = null!;
public string Prenom { get; set; } = null!;
public string Email { get; set; } = null!;
public string Profil { get; set; } = null!;
public int? ServiceId { get; set; }
// Manager direct, synchronisé depuis Entra ID (EntraSyncService) — c'est cette
// personne, et non le rôle FINANCE, qui a le droit de valider/refuser les demandes
// d'achat du collaborateur.
public int? ResponsableId { get; set; }
public bool Actif { get; set; }
public DateTime? DateCreation { get; set; }
public DateTime? DateDerniereConnexion { get; set; }
public virtual ICollection<Acompte> Acomptes { get; set; } = new List<Acompte>();
public virtual ICollection<AffectationsAvoir> AffectationsAvoirs { get; set; } = new List<AffectationsAvoir>();
public virtual ICollection<Audit> Audits { get; set; } = new List<Audit>();
public virtual ICollection<Avoir> Avoirs { get; set; } = new List<Avoir>();
public virtual ICollection<ContratFournisseur> ContratsFournisseurs { get; set; } = new List<ContratFournisseur>();
public virtual ICollection<ContratPrestataire> ContratsPrestataires { get; set; } = new List<ContratPrestataire>();
public virtual ICollection<DemandesAchat> DemandesAchatDemandeurs { get; set; } = new List<DemandesAchat>();
public virtual ICollection<DemandesAchat> DemandesAchatValideurs { get; set; } = new List<DemandesAchat>();
public virtual ICollection<Facture> FactureSaisiePars { get; set; } = new List<Facture>();
public virtual ICollection<Facture> FactureValideBapPars { get; set; } = new List<Facture>();
public virtual ICollection<FichiersXml> FichiersXmls { get; set; } = new List<FichiersXml>();
public virtual ICollection<HistoriqueAction> HistoriqueActions { get; set; } = new List<HistoriqueAction>();
public virtual ICollection<Paiement> Paiements { get; set; } = new List<Paiement>();
public virtual ICollection<Parametre> Parametres { get; set; } = new List<Parametre>();
public virtual ICollection<PiecesJointe> PiecesJointes { get; set; } = new List<PiecesJointe>();
public virtual ICollection<Rapprochement> Rapprochements { get; set; } = new List<Rapprochement>();
public virtual Service? Service { get; set; }
public virtual Utilisateur? Responsable { get; set; }
public virtual ICollection<Utilisateur> Collaborateurs { get; set; } = new List<Utilisateur>();
}
@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace SIFinance.Shared.Entities
{
public partial class UtilisateurRole
{
public string AzureOid { get; set; } = null!;
public string Role { get; set; } = null!;
}
}
@@ -0,0 +1,48 @@
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Options;
namespace SIFinance.Shared;
// Jeton Graph "app-only" (client_credentials) pour les appels serveur-à-serveur
// (synchro du groupe Entra ID, envoi d'emails de notification, etc.). Mis en cache 55
// minutes pour éviter un aller-retour Azure AD à chaque appel. Vit dans SIFinance.Shared
// (et non SIFinance.Api) car les modules en ont besoin eux aussi (ex. notification par
// email à la création d'une demande), et référencer SIFinance.Api depuis un module
// créerait un cycle.
public class EntraGraphTokenProvider(IHttpClientFactory httpClientFactory, IOptions<AzureAdOptions> options)
{
private string? _token;
private DateTime _expiresAt;
public async Task<string> GetTokenAsync(CancellationToken ct)
{
if (_token is not null && DateTime.UtcNow < _expiresAt) return _token;
var opts = options.Value;
var http = httpClientFactory.CreateClient();
var response = await http.PostAsync(
$"https://login.microsoftonline.com/{opts.TenantId}/oauth2/v2.0/token",
new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "client_credentials",
["client_id"] = opts.ClientId,
["client_secret"] = opts.ClientSecret,
["scope"] = "https://graph.microsoft.com/.default",
}), ct);
response.EnsureSuccessStatusCode();
var payload = await response.Content.ReadFromJsonAsync<TokenResponse>(cancellationToken: ct)
?? throw new InvalidOperationException("Réponse de jeton Graph vide.");
_token = payload.AccessToken;
_expiresAt = DateTime.UtcNow.AddMinutes(55);
return _token;
}
private class TokenResponse
{
[JsonPropertyName("access_token")]
public string AccessToken { get; set; } = "";
}
}
+145
View File
@@ -0,0 +1,145 @@
using System.Text;
using iText.Kernel.Colors;
using iText.Kernel.Pdf;
using iText.Kernel.Pdf.Filespec;
using iText.Layout;
using iText.Layout.Element;
using iText.Pdfa;
using Microsoft.Extensions.Configuration;
using s2industries.ZUGFeRD;
namespace SIFinance.Shared;
public record FacturXLigne(string Designation, decimal Quantite, decimal PrixUnitaireHt);
public record FacturXPartie(string Nom, string? Rue, string? CodePostal, string? Ville, string PaysCode, string? NumeroTva);
public record FacturXEmissionRequest(
string NumeroFacture,
DateOnly DateFacture,
FacturXPartie Vendeur,
FacturXPartie Acheteur,
List<FacturXLigne> Lignes,
decimal TauxTva);
// Moteur réutilisable d'émission de factures électroniques Factur-X/ZUGFeRD (norme
// EN16931) : XML structuré via ZUGFeRD-csharp, assemblage PDF/A-3 + attachement du XML
// via iText7. Vit dans SIFinance.Shared pour être appelable depuis n'importe quel
// module (Achats, Sous-Traitants, futur Contrat Étudiant) une fois qu'un cas d'usage
// concret d'émission est identifié — voir appels dans les endpoints des modules.
//
// Nécessite un profil colorimétrique ICC sRGB valide (obligatoire pour la conformité
// PDF/A-3, "PdfOutputIntent") — fichier binaire non fourni ici (voir
// SIFinance.Shared/Resources/README.md pour comment se le procurer et où le déposer).
public class FacturXEmissionService(IConfiguration configuration)
{
private string CheminProfilIcc =>
configuration["FacturX:ProfilIccSrgb"] ?? Path.Combine(AppContext.BaseDirectory, "Resources", "sRGB.icc");
public byte[] Generer(FacturXEmissionRequest request)
{
if (request.Lignes.Count == 0)
throw new ArgumentException("Une facture électronique doit contenir au moins une ligne.", nameof(request));
var montantHt = request.Lignes.Sum(l => Math.Round(l.Quantite * l.PrixUnitaireHt, 2));
var montantTva = Math.Round(montantHt * request.TauxTva / 100m, 2);
var montantTtc = montantHt + montantTva;
var xmlBytes = GenererXml(request, montantHt, montantTva, montantTtc);
return AssemblerPdfA3(request, xmlBytes, montantHt, montantTva, montantTtc);
}
private static byte[] GenererXml(FacturXEmissionRequest request, decimal montantHt, decimal montantTva, decimal montantTtc)
{
var desc = InvoiceDescriptor.CreateInvoice(request.NumeroFacture, request.DateFacture.ToDateTime(TimeOnly.MinValue), CurrencyCodes.EUR);
desc.SetSeller(
request.Vendeur.Nom, request.Vendeur.CodePostal ?? "", request.Vendeur.Ville ?? "", request.Vendeur.Rue ?? "",
ParsePays(request.Vendeur.PaysCode), request.Vendeur.NumeroTva);
desc.SetBuyer(
request.Acheteur.Nom, request.Acheteur.CodePostal ?? "", request.Acheteur.Ville ?? "", request.Acheteur.Rue ?? "",
ParsePays(request.Acheteur.PaysCode), request.Acheteur.NumeroTva);
foreach (var ligne in request.Lignes)
{
desc.AddTradeLineItem(
name: ligne.Designation,
netUnitPrice: ligne.PrixUnitaireHt,
unitCode: QuantityCodes.C62,
billedQuantity: ligne.Quantite,
taxType: TaxTypes.VAT,
categoryCode: TaxCategoryCodes.S,
taxPercent: request.TauxTva);
}
desc.AddApplicableTradeTax(montantHt, request.TauxTva, montantTva, TaxTypes.VAT, TaxCategoryCodes.S);
desc.SetTotals(
lineTotalAmount: montantHt, taxBasisAmount: montantHt, taxTotalAmount: montantTva,
grandTotalAmount: montantTtc, duePayableAmount: montantTtc);
using var xmlStream = new MemoryStream();
desc.Save(xmlStream, ZUGFeRDVersion.Version23, Profile.Comfort);
return xmlStream.ToArray();
}
private byte[] AssemblerPdfA3(FacturXEmissionRequest request, byte[] xmlBytes, decimal montantHt, decimal montantTva, decimal montantTtc)
{
var cheminIcc = CheminProfilIcc;
if (!File.Exists(cheminIcc))
throw new InvalidOperationException(
$"Profil ICC sRGB introuvable ({cheminIcc}) — requis pour générer un PDF/A-3 conforme. " +
"Voir SIFinance.Shared/Resources/README.md.");
using var pdfMemStream = new MemoryStream();
using (var iccStream = File.OpenRead(cheminIcc))
{
var outputIntent = new PdfOutputIntent("Custom", "", "http://www.color.org", "sRGB IEC61966-2.1", iccStream);
using var writer = new PdfWriter(pdfMemStream);
using var pdfDoc = new PdfADocument(writer, PdfAConformance.PDF_A_3B, outputIntent);
pdfDoc.SetTagged();
using (var document = new Document(pdfDoc))
{
document.Add(new Paragraph($"Facture {request.NumeroFacture}").SetFontSize(18).SimulateBold());
document.Add(new Paragraph($"Date : {request.DateFacture:dd/MM/yyyy}"));
document.Add(new Paragraph(" "));
document.Add(new Paragraph("Émetteur").SimulateBold());
document.Add(new Paragraph($"{request.Vendeur.Nom}\n{request.Vendeur.Rue}\n{request.Vendeur.CodePostal} {request.Vendeur.Ville}"));
document.Add(new Paragraph(" "));
document.Add(new Paragraph("Destinataire").SimulateBold());
document.Add(new Paragraph($"{request.Acheteur.Nom}\n{request.Acheteur.Rue}\n{request.Acheteur.CodePostal} {request.Acheteur.Ville}"));
document.Add(new Paragraph(" "));
var table = new Table(4).UseAllAvailableWidth();
table.AddHeaderCell("Désignation");
table.AddHeaderCell("Qté");
table.AddHeaderCell("PU HT");
table.AddHeaderCell("Total HT");
foreach (var ligne in request.Lignes)
{
table.AddCell(ligne.Designation);
table.AddCell(ligne.Quantite.ToString("N2"));
table.AddCell($"{ligne.PrixUnitaireHt:N2} €");
table.AddCell($"{(ligne.Quantite * ligne.PrixUnitaireHt):N2} €");
}
document.Add(table);
document.Add(new Paragraph(" "));
document.Add(new Paragraph($"Total HT : {montantHt:N2} €"));
document.Add(new Paragraph($"TVA ({request.TauxTva:N0}%) : {montantTva:N2} €"));
document.Add(new Paragraph($"Total TTC : {montantTtc:N2} €").SimulateBold());
}
var fileSpec = PdfFileSpec.CreateEmbeddedFileSpec(
pdfDoc, xmlBytes, "Facture électronique Factur-X", "factur-x.xml", PdfName.ApplicationXml, null, PdfName.Data);
pdfDoc.AddFileAttachment("factur-x.xml", fileSpec);
}
return pdfMemStream.ToArray();
}
private static s2industries.ZUGFeRD.CountryCodes ParsePays(string code) =>
Enum.TryParse<s2industries.ZUGFeRD.CountryCodes>(code, true, out var parsed) ? parsed : s2industries.ZUGFeRD.CountryCodes.FR;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,148 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SIFinance.Shared.Migrations
{
/// <inheritdoc />
public partial class AddContratsAndPaiementStatut : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "statut",
schema: "fin",
table: "paiements",
type: "varchar(20)",
unicode: false,
maxLength: 20,
nullable: false,
defaultValue: "PROGRAMME");
migrationBuilder.AddColumn<bool>(
name: "rapproche",
schema: "fin",
table: "paiements",
type: "bit",
nullable: false,
defaultValue: false);
migrationBuilder.CreateTable(
name: "contrats_fournisseurs",
schema: "achats",
columns: table => new
{
id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
fournisseur_id = table.Column<int>(type: "int", nullable: false),
reference = table.Column<string>(type: "varchar(50)", unicode: false, maxLength: 50, nullable: true),
objet = table.Column<string>(type: "varchar(255)", unicode: false, maxLength: 255, nullable: false),
date_signature = table.Column<DateOnly>(type: "date", nullable: true),
date_debut = table.Column<DateOnly>(type: "date", nullable: false),
date_fin = table.Column<DateOnly>(type: "date", nullable: false),
montant_total = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
periodicite = table.Column<string>(type: "varchar(20)", unicode: false, maxLength: 20, nullable: false, defaultValue: "mensuelle"),
statut = table.Column<string>(type: "varchar(20)", unicode: false, maxLength: 20, nullable: false, defaultValue: "ACTIF"),
saisie_par_id = table.Column<int>(type: "int", nullable: false),
date_creation = table.Column<DateTime>(type: "datetime", nullable: true, defaultValueSql: "(getdate())"),
date_derniere_maj = table.Column<DateTime>(type: "datetime", nullable: true, defaultValueSql: "(getdate())")
},
constraints: table =>
{
table.PrimaryKey("PK__contrats_fournisseurs", x => x.id);
table.ForeignKey(
name: "FK__contrats_fournisseurs__fournisseur",
column: x => x.fournisseur_id,
principalSchema: "ref",
principalTable: "fournisseurs",
principalColumn: "id");
table.ForeignKey(
name: "FK__contrats_fournisseurs__saisie_par",
column: x => x.saisie_par_id,
principalSchema: "core",
principalTable: "utilisateurs",
principalColumn: "id");
});
migrationBuilder.CreateTable(
name: "contrats_echeances",
schema: "achats",
columns: table => new
{
id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
contrat_id = table.Column<int>(type: "int", nullable: false),
date_echeance = table.Column<DateOnly>(type: "date", nullable: false),
montant = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
statut = table.Column<string>(type: "varchar(20)", unicode: false, maxLength: 20, nullable: false, defaultValue: "A_VENIR"),
justificatif_id = table.Column<int>(type: "int", nullable: true),
date_creation = table.Column<DateTime>(type: "datetime", nullable: true, defaultValueSql: "(getdate())")
},
constraints: table =>
{
table.PrimaryKey("PK__contrats_echeances", x => x.id);
table.ForeignKey(
name: "FK__contrats_echeances__contrat",
column: x => x.contrat_id,
principalSchema: "achats",
principalTable: "contrats_fournisseurs",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK__contrats_echeances__justificatif",
column: x => x.justificatif_id,
principalSchema: "core",
principalTable: "pieces_jointes",
principalColumn: "id");
});
migrationBuilder.CreateIndex(
name: "IX_ctf_fournisseur",
schema: "achats",
table: "contrats_fournisseurs",
column: "fournisseur_id");
migrationBuilder.CreateIndex(
name: "IX_ctf_saisie_par",
schema: "achats",
table: "contrats_fournisseurs",
column: "saisie_par_id");
migrationBuilder.CreateIndex(
name: "IX_cte_contrat",
schema: "achats",
table: "contrats_echeances",
column: "contrat_id");
migrationBuilder.CreateIndex(
name: "IX_cte_justif",
schema: "achats",
table: "contrats_echeances",
column: "justificatif_id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "contrats_echeances",
schema: "achats");
migrationBuilder.DropTable(
name: "contrats_fournisseurs",
schema: "achats");
migrationBuilder.DropColumn(
name: "rapproche",
schema: "fin",
table: "paiements");
migrationBuilder.DropColumn(
name: "statut",
schema: "fin",
table: "paiements");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,31 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SIFinance.Shared.Migrations
{
/// <inheritdoc />
public partial class AddCategorieFournisseurLibre : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "categorie_fournisseur_libre",
schema: "achats",
table: "demandes_achat",
type: "nvarchar(100)",
maxLength: 100,
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "categorie_fournisseur_libre",
schema: "achats",
table: "demandes_achat");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,104 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SIFinance.Shared.Migrations
{
/// <inheritdoc />
public partial class AddSousTraitantsOrganismesContrats : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.EnsureSchema(
name: "st");
migrationBuilder.CreateTable(
name: "organismes",
schema: "st",
columns: table => new
{
id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
raison_sociale = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false),
siret = table.Column<string>(type: "varchar(20)", unicode: false, maxLength: 20, nullable: true),
email = table.Column<string>(type: "varchar(255)", unicode: false, maxLength: 255, nullable: true),
telephone = table.Column<string>(type: "varchar(30)", unicode: false, maxLength: 30, nullable: true),
adresse = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: true),
statut = table.Column<string>(type: "varchar(20)", unicode: false, maxLength: 20, nullable: false, defaultValue: "ACTIF"),
date_creation = table.Column<DateTime>(type: "datetime", nullable: true, defaultValueSql: "(getdate())")
},
constraints: table =>
{
table.PrimaryKey("PK__organismes", x => x.id);
});
migrationBuilder.CreateTable(
name: "contrats_prestataires",
schema: "st",
columns: table => new
{
id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
organisme_id = table.Column<int>(type: "int", nullable: false),
reference = table.Column<string>(type: "varchar(50)", unicode: false, maxLength: 50, nullable: false),
objet = table.Column<string>(type: "nvarchar(255)", maxLength: 255, nullable: false),
date_signature = table.Column<DateOnly>(type: "date", nullable: true),
date_debut = table.Column<DateOnly>(type: "date", nullable: false),
date_fin = table.Column<DateOnly>(type: "date", nullable: false),
montant_total = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
statut = table.Column<string>(type: "varchar(20)", unicode: false, maxLength: 20, nullable: false, defaultValue: "ACTIF"),
saisie_par_id = table.Column<int>(type: "int", nullable: false),
date_creation = table.Column<DateTime>(type: "datetime", nullable: true, defaultValueSql: "(getdate())")
},
constraints: table =>
{
table.PrimaryKey("PK__contrats_prestataires", x => x.id);
table.ForeignKey(
name: "FK__contrats_prestataires__organisme",
column: x => x.organisme_id,
principalSchema: "st",
principalTable: "organismes",
principalColumn: "id");
table.ForeignKey(
name: "FK__contrats_prestataires__saisie_par",
column: x => x.saisie_par_id,
principalSchema: "core",
principalTable: "utilisateurs",
principalColumn: "id");
});
migrationBuilder.CreateIndex(
name: "IX_ctp_organisme",
schema: "st",
table: "contrats_prestataires",
column: "organisme_id");
migrationBuilder.CreateIndex(
name: "IX_ctp_saisie_par",
schema: "st",
table: "contrats_prestataires",
column: "saisie_par_id");
migrationBuilder.CreateIndex(
name: "UQ_ctp_reference",
schema: "st",
table: "contrats_prestataires",
column: "reference",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "contrats_prestataires",
schema: "st");
migrationBuilder.DropTable(
name: "organismes",
schema: "st");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,55 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SIFinance.Shared.Migrations
{
/// <inheritdoc />
public partial class AddResponsableToUtilisateurs : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "responsable_id",
schema: "core",
table: "utilisateurs",
type: "int",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_utilisateurs_responsable_id",
schema: "core",
table: "utilisateurs",
column: "responsable_id");
migrationBuilder.AddForeignKey(
name: "FK__utilisateurs__responsable",
schema: "core",
table: "utilisateurs",
column: "responsable_id",
principalSchema: "core",
principalTable: "utilisateurs",
principalColumn: "id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK__utilisateurs__responsable",
schema: "core",
table: "utilisateurs");
migrationBuilder.DropIndex(
name: "IX_utilisateurs_responsable_id",
schema: "core",
table: "utilisateurs");
migrationBuilder.DropColumn(
name: "responsable_id",
schema: "core",
table: "utilisateurs");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,31 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SIFinance.Shared.Migrations
{
/// <inheritdoc />
public partial class AddCommentaireToFactures : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "commentaire",
schema: "fin",
table: "factures",
type: "nvarchar(1000)",
maxLength: 1000,
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "commentaire",
schema: "fin",
table: "factures");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,56 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SIFinance.Shared.Migrations
{
/// <inheritdoc />
public partial class AddContratIdToFactures : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "contrat_id",
schema: "fin",
table: "factures",
type: "int",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_factures_contrat_id",
schema: "fin",
table: "factures",
column: "contrat_id");
migrationBuilder.AddForeignKey(
name: "FK_factures_contrat",
schema: "fin",
table: "factures",
column: "contrat_id",
principalSchema: "achats",
principalTable: "contrats_fournisseurs",
principalColumn: "id",
onDelete: ReferentialAction.SetNull);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_factures_contrat",
schema: "fin",
table: "factures");
migrationBuilder.DropIndex(
name: "IX_factures_contrat_id",
schema: "fin",
table: "factures");
migrationBuilder.DropColumn(
name: "contrat_id",
schema: "fin",
table: "factures");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,84 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SIFinance.Shared.Migrations
{
/// <inheritdoc />
public partial class AddBudgetsService : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "budgets_service",
schema: "achats",
columns: table => new
{
id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
service_id = table.Column<int>(type: "int", nullable: false),
exercice_annee = table.Column<int>(type: "int", nullable: false),
montant_initial = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
montant_revise = table.Column<decimal>(type: "decimal(18,2)", nullable: true),
date_revision = table.Column<DateTime>(type: "datetime", nullable: true),
revise_par = table.Column<int>(type: "int", nullable: true),
commentaire_revision = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true),
saisie_par_id = table.Column<int>(type: "int", nullable: false),
date_creation = table.Column<DateTime>(type: "datetime", nullable: true, defaultValueSql: "(getdate())"),
date_derniere_maj = table.Column<DateTime>(type: "datetime", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_budgets_service", x => x.id);
table.ForeignKey(
name: "FK_budgets_service_revise_par",
column: x => x.revise_par,
principalSchema: "core",
principalTable: "utilisateurs",
principalColumn: "id",
onDelete: ReferentialAction.SetNull);
table.ForeignKey(
name: "FK_budgets_service_saisie_par",
column: x => x.saisie_par_id,
principalSchema: "core",
principalTable: "utilisateurs",
principalColumn: "id");
table.ForeignKey(
name: "FK_budgets_service_service",
column: x => x.service_id,
principalSchema: "ref",
principalTable: "services",
principalColumn: "id");
});
migrationBuilder.CreateIndex(
name: "IX_budgets_service_revise_par",
schema: "achats",
table: "budgets_service",
column: "revise_par");
migrationBuilder.CreateIndex(
name: "IX_budgets_service_saisie_par_id",
schema: "achats",
table: "budgets_service",
column: "saisie_par_id");
migrationBuilder.CreateIndex(
name: "IX_budgets_service_unique",
schema: "achats",
table: "budgets_service",
columns: new[] { "service_id", "exercice_annee" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "budgets_service",
schema: "achats");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,141 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SIFinance.Shared.Migrations
{
/// <inheritdoc />
public partial class SplitBudgetRevisionsHistory : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_budgets_service_revise_par",
schema: "achats",
table: "budgets_service");
migrationBuilder.DropIndex(
name: "IX_budgets_service_revise_par",
schema: "achats",
table: "budgets_service");
migrationBuilder.DropColumn(
name: "commentaire_revision",
schema: "achats",
table: "budgets_service");
migrationBuilder.DropColumn(
name: "date_revision",
schema: "achats",
table: "budgets_service");
migrationBuilder.DropColumn(
name: "montant_revise",
schema: "achats",
table: "budgets_service");
migrationBuilder.DropColumn(
name: "revise_par",
schema: "achats",
table: "budgets_service");
migrationBuilder.CreateTable(
name: "budgets_service_revisions",
schema: "achats",
columns: table => new
{
id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
budget_service_id = table.Column<int>(type: "int", nullable: false),
montant_revise = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
commentaire = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true),
revise_par = table.Column<int>(type: "int", nullable: false),
date_revision = table.Column<DateTime>(type: "datetime", nullable: false, defaultValueSql: "(getdate())")
},
constraints: table =>
{
table.PrimaryKey("PK_budgets_service_revisions", x => x.id);
table.ForeignKey(
name: "FK_budgets_service_revisions_budget",
column: x => x.budget_service_id,
principalSchema: "achats",
principalTable: "budgets_service",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_budgets_service_revisions_revise_par",
column: x => x.revise_par,
principalSchema: "core",
principalTable: "utilisateurs",
principalColumn: "id");
});
migrationBuilder.CreateIndex(
name: "IX_budgets_service_revisions_budget_service_id",
schema: "achats",
table: "budgets_service_revisions",
column: "budget_service_id");
migrationBuilder.CreateIndex(
name: "IX_budgets_service_revisions_revise_par",
schema: "achats",
table: "budgets_service_revisions",
column: "revise_par");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "budgets_service_revisions",
schema: "achats");
migrationBuilder.AddColumn<string>(
name: "commentaire_revision",
schema: "achats",
table: "budgets_service",
type: "nvarchar(1000)",
maxLength: 1000,
nullable: true);
migrationBuilder.AddColumn<DateTime>(
name: "date_revision",
schema: "achats",
table: "budgets_service",
type: "datetime",
nullable: true);
migrationBuilder.AddColumn<decimal>(
name: "montant_revise",
schema: "achats",
table: "budgets_service",
type: "decimal(18,2)",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "revise_par",
schema: "achats",
table: "budgets_service",
type: "int",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_budgets_service_revise_par",
schema: "achats",
table: "budgets_service",
column: "revise_par");
migrationBuilder.AddForeignKey(
name: "FK_budgets_service_revise_par",
schema: "achats",
table: "budgets_service",
column: "revise_par",
principalSchema: "core",
principalTable: "utilisateurs",
principalColumn: "id",
onDelete: ReferentialAction.SetNull);
}
}
}
@@ -0,0 +1,18 @@
-- À exécuter une seule fois sur la base SIFinance.
-- Le flag permet de rattacher n'importe quelle catégorie au circuit matériel,
-- sans dépendre de son libellé.
ALTER TABLE ref.types_depense
ADD est_materiel bit NOT NULL
CONSTRAINT DF_types_depense_est_materiel DEFAULT (0);
GO
-- Les nouveaux statuts du workflow dépassent parfois la taille historique de 30.
ALTER TABLE achats.demandes_achat
ALTER COLUMN statut varchar(50) NOT NULL;
GO
-- Catégories à traiter comme du matériel : adapte cette liste à tes règles métier.
UPDATE ref.types_depense
SET est_materiel = 1
WHERE libelle IN ('Fournitures', 'Informatique', 'Mobilier');
GO
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,50 @@
using System.Net.Http.Headers;
using System.Net.Http.Json;
using Microsoft.Extensions.Logging;
namespace SIFinance.Shared;
// Envoie des notifications par email via Microsoft Graph (permission application
// Mail.Send, déjà accordée) — envoie "au nom de" une boîte du tenant (ex. le
// demandeur), pas besoin de boîte de service dédiée. Les échecs sont journalisés mais
// n'interrompent jamais le flux appelant (une notification ratée ne doit pas faire
// échouer la création d'une demande). Vit dans SIFinance.Shared car les modules
// (Achats, Sous-Traitants...) doivent pouvoir l'appeler directement.
public class NotificationEmailService(
IHttpClientFactory httpClientFactory,
EntraGraphTokenProvider tokenProvider,
ILogger<NotificationEmailService> logger)
{
public async Task EnvoyerAsync(string expediteurEmail, string destinataireEmail, string sujet, string corpsHtml, CancellationToken ct)
{
try
{
var token = await tokenProvider.GetTokenAsync(ct);
var http = httpClientFactory.CreateClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var payload = new
{
message = new
{
subject = sujet,
body = new { contentType = "HTML", content = corpsHtml },
toRecipients = new[] { new { emailAddress = new { address = destinataireEmail } } },
},
saveToSentItems = false,
};
var response = await http.PostAsJsonAsync(
$"https://graph.microsoft.com/v1.0/users/{Uri.EscapeDataString(expediteurEmail)}/sendMail", payload, ct);
if (!response.IsSuccessStatusCode)
{
var body = await response.Content.ReadAsStringAsync(ct);
logger.LogWarning("Échec d'envoi d'email de notification ({Status}) : {Body}", response.StatusCode, body);
}
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
logger.LogWarning(ex, "Échec d'envoi d'email de notification à {Destinataire}.", destinataireEmail);
}
}
}
+28
View File
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="itext7" Version="9.7.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.9">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.9" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.9">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="ZUGFeRD-csharp" Version="18.0.0" />
</ItemGroup>
<ItemGroup>
<!-- Nécessaire pour IHttpContextAccessor (CurrentUserService), partagé par l'API et les modules. -->
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
</Project>