Compare commits
4 Commits
1a7d5cb64e
...
deb5b5735c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
deb5b5735c | ||
|
|
a0c440dfae | ||
|
|
a88b342a0d | ||
|
|
7e1253615b |
2
app.json
2
app.json
@@ -8,7 +8,7 @@
|
|||||||
},
|
},
|
||||||
"containerName": "demat-facturation-app",
|
"containerName": "demat-facturation-app",
|
||||||
"image": "images/demat-facturation-dsi.jpg",
|
"image": "images/demat-facturation-dsi.jpg",
|
||||||
"giteaRepo": "demat-facturation-dsi",
|
"giteaRepo": "demat-facturation",
|
||||||
"giteaOwner": "manus-admin",
|
"giteaOwner": "manus-admin",
|
||||||
"ci": {
|
"ci": {
|
||||||
"required": true
|
"required": true
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { trpc } from "@/lib/trpc";
|
import { trpc } from "@/lib/trpc";
|
||||||
|
import { BAP_MIN_QUALITY_SCORE, meetsBapQualityThreshold } from "@shared/bapEligibility";
|
||||||
import { Search, FileText, Download, FileSpreadsheet, Trash2, Edit, Trash, CheckCircle, CheckCircle2, ShieldCheck, RefreshCw, FolderDown, ChevronDown, ChevronRight, ChevronsUpDown, CalendarDays, ArrowUpDown, ArrowUp, ArrowDown } from "lucide-react";
|
import { Search, FileText, Download, FileSpreadsheet, Trash2, Edit, Trash, CheckCircle, CheckCircle2, ShieldCheck, RefreshCw, FolderDown, ChevronDown, ChevronRight, ChevronsUpDown, CalendarDays, ArrowUpDown, ArrowUp, ArrowDown } from "lucide-react";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
|
||||||
@@ -394,7 +395,7 @@ export default function InvoicesBAP() {
|
|||||||
// Déclarée ici (avant filteredInvoices et statusCounts) pour éviter le hoisting error
|
// Déclarée ici (avant filteredInvoices et statusCounts) pour éviter le hoisting error
|
||||||
const isEligibleForBAPValidation = (invoice: any) => {
|
const isEligibleForBAPValidation = (invoice: any) => {
|
||||||
return (
|
return (
|
||||||
(invoice.qualityScore || 0) === 100 &&
|
meetsBapQualityThreshold(invoice.qualityScore) &&
|
||||||
invoice.exportStatus !== "exported" &&
|
invoice.exportStatus !== "exported" &&
|
||||||
invoice.isSubscription === 0 &&
|
invoice.isSubscription === 0 &&
|
||||||
invoice.serviceConcerne &&
|
invoice.serviceConcerne &&
|
||||||
@@ -526,19 +527,19 @@ export default function InvoicesBAP() {
|
|||||||
|
|
||||||
const getQualityBadge = (score: number | null) => {
|
const getQualityBadge = (score: number | null) => {
|
||||||
if (score === null) return <Badge variant="outline">-</Badge>;
|
if (score === null) return <Badge variant="outline">-</Badge>;
|
||||||
if (score === 100) return <Badge className="bg-green-100 text-green-800 hover:bg-green-100">{score}%</Badge>;
|
if (meetsBapQualityThreshold(score)) return <Badge className="bg-green-100 text-green-800 hover:bg-green-100">{score}%</Badge>;
|
||||||
if (score >= 80) return <Badge className="bg-yellow-100 text-yellow-800 hover:bg-yellow-100">{score}%</Badge>;
|
if (score >= 80) return <Badge className="bg-yellow-100 text-yellow-800 hover:bg-yellow-100">{score}%</Badge>;
|
||||||
return <Badge className="bg-red-100 text-red-800 hover:bg-red-100">{score}%</Badge>;
|
return <Badge className="bg-red-100 text-red-800 hover:bg-red-100">{score}%</Badge>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isEligibleForExport = (invoice: any) => {
|
const isEligibleForExport = (invoice: any) => {
|
||||||
// Une facture BAP est exportable si :
|
// Une facture BAP est exportable si :
|
||||||
// 1. Score = 100%
|
// 1. Score >= 90 % (seuil BAP)
|
||||||
// 2. Type d'achat rempli
|
// 2. Type d'achat rempli
|
||||||
// 3. Service concerné rempli
|
// 3. Service concerné rempli
|
||||||
// 4. Ventilation comptable remplie
|
// 4. Ventilation comptable remplie
|
||||||
return (
|
return (
|
||||||
(invoice.qualityScore || 0) === 100 &&
|
meetsBapQualityThreshold(invoice.qualityScore) &&
|
||||||
invoice.typeAchat &&
|
invoice.typeAchat &&
|
||||||
invoice.serviceConcerne &&
|
invoice.serviceConcerne &&
|
||||||
invoice.ventilationComptable
|
invoice.ventilationComptable
|
||||||
@@ -549,7 +550,7 @@ export default function InvoicesBAP() {
|
|||||||
|
|
||||||
const getBAPValidationTooltip = (invoice: any): string => {
|
const getBAPValidationTooltip = (invoice: any): string => {
|
||||||
const reasons: string[] = [];
|
const reasons: string[] = [];
|
||||||
if ((invoice.qualityScore || 0) < 100) reasons.push("Score < 100%");
|
if (!meetsBapQualityThreshold(invoice.qualityScore)) reasons.push(`Score < ${BAP_MIN_QUALITY_SCORE}%`);
|
||||||
if (invoice.exportStatus === "exported") reasons.push("Déjà exportée");
|
if (invoice.exportStatus === "exported") reasons.push("Déjà exportée");
|
||||||
if (invoice.isSubscription !== 0) reasons.push("Marquée comme abonnement");
|
if (invoice.isSubscription !== 0) reasons.push("Marquée comme abonnement");
|
||||||
if (!invoice.serviceConcerne) reasons.push("Service manquant");
|
if (!invoice.serviceConcerne) reasons.push("Service manquant");
|
||||||
@@ -655,7 +656,7 @@ export default function InvoicesBAP() {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (confirm("Valider en BAP toutes les factures éligibles (score 100%, champs remplis, non abonnement) ? Les PDFs annotés seront générés automatiquement.")) {
|
if (confirm(`Valider en BAP toutes les factures éligibles (score ≥ ${BAP_MIN_QUALITY_SCORE}%, champs remplis, non abonnement) ? Les PDFs annotés seront générés automatiquement.`)) {
|
||||||
validateBAPBulkMutation.mutate();
|
validateBAPBulkMutation.mutate();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
16
server/bapEligibility.test.ts
Normal file
16
server/bapEligibility.test.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { BAP_MIN_QUALITY_SCORE, meetsBapQualityThreshold } from "@shared/bapEligibility";
|
||||||
|
|
||||||
|
describe("meetsBapQualityThreshold", () => {
|
||||||
|
it("autorise exactement le seuil de 90 %", () => {
|
||||||
|
expect(BAP_MIN_QUALITY_SCORE).toBe(90);
|
||||||
|
expect(meetsBapQualityThreshold(90)).toBe(true);
|
||||||
|
expect(meetsBapQualityThreshold(100)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("refuse les scores inférieurs ou absents", () => {
|
||||||
|
expect(meetsBapQualityThreshold(89)).toBe(false);
|
||||||
|
expect(meetsBapQualityThreshold(null)).toBe(false);
|
||||||
|
expect(meetsBapQualityThreshold(undefined)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { COOKIE_NAME } from "@shared/const";
|
import { COOKIE_NAME } from "@shared/const";
|
||||||
|
import { BAP_MIN_QUALITY_SCORE, meetsBapQualityThreshold } from "@shared/bapEligibility";
|
||||||
|
|
||||||
interface Condition {
|
interface Condition {
|
||||||
field: string;
|
field: string;
|
||||||
@@ -499,8 +500,8 @@ export const appRouter = router({
|
|||||||
const hasService = !!invoice.serviceConcerne;
|
const hasService = !!invoice.serviceConcerne;
|
||||||
const hasTypeAchat = !!invoice.typeAchat;
|
const hasTypeAchat = !!invoice.typeAchat;
|
||||||
const hasVentilation = !!invoice.ventilationComptable;
|
const hasVentilation = !!invoice.ventilationComptable;
|
||||||
if (score < 100) {
|
if (!meetsBapQualityThreshold(score)) {
|
||||||
throw new TRPCError({ code: "BAD_REQUEST", message: "Le score de qualité doit être à 100% pour valider" });
|
throw new TRPCError({ code: "BAD_REQUEST", message: `Le score de qualité doit être au moins de ${BAP_MIN_QUALITY_SCORE}% pour valider` });
|
||||||
}
|
}
|
||||||
if (!isNotSubscription) {
|
if (!isNotSubscription) {
|
||||||
throw new TRPCError({ code: "BAD_REQUEST", message: "La facture est marquée comme abonnement" });
|
throw new TRPCError({ code: "BAD_REQUEST", message: "La facture est marquée comme abonnement" });
|
||||||
@@ -720,7 +721,7 @@ export const appRouter = router({
|
|||||||
const allInvoices = ctx.user.role === 'admin' ? await getAllInvoices() : await getInvoicesByUser(ctx.user.id);
|
const allInvoices = ctx.user.role === 'admin' ? await getAllInvoices() : await getInvoicesByUser(ctx.user.id);
|
||||||
// Filtrer les factures éligibles (non déjà validées)
|
// Filtrer les factures éligibles (non déjà validées)
|
||||||
const eligible = allInvoices.filter((inv: any) =>
|
const eligible = allInvoices.filter((inv: any) =>
|
||||||
(inv.qualityScore || 0) >= 100 &&
|
meetsBapQualityThreshold(inv.qualityScore) &&
|
||||||
inv.isSubscription === 0 &&
|
inv.isSubscription === 0 &&
|
||||||
!!inv.serviceConcerne &&
|
!!inv.serviceConcerne &&
|
||||||
!!inv.typeAchat &&
|
!!inv.typeAchat &&
|
||||||
|
|||||||
10
shared/bapEligibility.ts
Normal file
10
shared/bapEligibility.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/** Score minimal requis pour générer et valider un BAP. */
|
||||||
|
export const BAP_MIN_QUALITY_SCORE = 90;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Centralise le seuil BAP afin que l’interface et le serveur appliquent la
|
||||||
|
* même règle métier, y compris pour les scores nuls ou absents.
|
||||||
|
*/
|
||||||
|
export function meetsBapQualityThreshold(score: number | null | undefined): boolean {
|
||||||
|
return typeof score === "number" && Number.isFinite(score) && score >= BAP_MIN_QUALITY_SCORE;
|
||||||
|
}
|
||||||
17
todo.md
17
todo.md
@@ -778,7 +778,18 @@
|
|||||||
- [x] Ajouter les tests et valider TypeScript, tests et build
|
- [x] Ajouter les tests et valider TypeScript, tests et build
|
||||||
|
|
||||||
## Déploiement — filtre par action des automatismes
|
## Déploiement — filtre par action des automatismes
|
||||||
- [ ] Pousser le filtre par action vers Gitea recette
|
- [x] Pousser le filtre par action vers Gitea recette
|
||||||
- [ ] Déployer et vérifier le filtre par action en recette
|
- [x] Déployer et vérifier le filtre par action en recette
|
||||||
|
- [x] Pousser la version validée vers Gitea production
|
||||||
|
- [x] Déployer et vérifier le filtre par action en production
|
||||||
|
|
||||||
|
## Seuil d’éligibilité BAP à 90 %
|
||||||
|
- [x] Identifier les validations BAP fondées sur le score de qualité
|
||||||
|
- [x] Abaisser le seuil de 100 % à 90 % pour les opérations BAP
|
||||||
|
- [x] Ajouter les tests de seuil et valider TypeScript, tests et build
|
||||||
|
|
||||||
|
## Déploiement — seuil BAP à 90 %
|
||||||
|
- [ ] Pousser le correctif vers Gitea recette
|
||||||
|
- [ ] Déployer et vérifier le seuil BAP à 90 % en recette
|
||||||
- [ ] Pousser la version validée vers Gitea production
|
- [ ] Pousser la version validée vers Gitea production
|
||||||
- [ ] Déployer et vérifier le filtre par action en production
|
- [ ] Déployer et vérifier le seuil BAP à 90 % en production
|
||||||
|
|||||||
Reference in New Issue
Block a user