Checkpoint: Ajout du champ Abonnement (OUI/NON) aux factures avec détection automatique par mots-clés et édition directe dans la liste
This commit is contained in:
@@ -409,6 +409,7 @@ export default function Invoices() {
|
||||
<TableHead>Service</TableHead>
|
||||
<TableHead>Type achat</TableHead>
|
||||
<TableHead>Ventilation</TableHead>
|
||||
<TableHead>Abonnement</TableHead>
|
||||
<TableHead>Score</TableHead>
|
||||
<TableHead>Statut</TableHead>
|
||||
<TableHead className="w-32">Actions</TableHead>
|
||||
@@ -509,6 +510,21 @@ export default function Invoices() {
|
||||
<option value="__ADD_NEW__" className="font-semibold text-blue-600">➜ Ajouter...</option>
|
||||
</select>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">
|
||||
<select
|
||||
value={invoice.isSubscription ? "OUI" : "NON"}
|
||||
onChange={(e) => {
|
||||
updateFieldMutation.mutate({
|
||||
id: invoice.id,
|
||||
data: { isSubscription: e.target.value === "OUI" ? 1 : 0 },
|
||||
});
|
||||
}}
|
||||
className="w-full px-2 py-1 border rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="NON">NON</option>
|
||||
<option value="OUI">OUI</option>
|
||||
</select>
|
||||
</TableCell>
|
||||
<TableCell>{getQualityBadge(invoice.qualityScore)}</TableCell>
|
||||
<TableCell>{getExportStatusBadge(invoice.exportStatus || "not_exported")}</TableCell>
|
||||
<TableCell>
|
||||
|
||||
@@ -20,6 +20,7 @@ export default function Settings() {
|
||||
const [orderNumberKeywords, setOrderNumberKeywords] = useState("");
|
||||
const [supplierKeywords, setSupplierKeywords] = useState("");
|
||||
const [totalAmountKeywords, setTotalAmountKeywords] = useState("");
|
||||
const [subscriptionKeywords, setSubscriptionKeywords] = useState("");
|
||||
const [sftpHost, setSftpHost] = useState("");
|
||||
const [sftpPort, setSftpPort] = useState(22);
|
||||
const [sftpUsername, setSftpUsername] = useState("");
|
||||
@@ -36,6 +37,7 @@ export default function Settings() {
|
||||
setOrderNumberKeywords(settings.orderNumberKeywords || "");
|
||||
setSupplierKeywords(settings.supplierKeywords || "");
|
||||
setTotalAmountKeywords(settings.totalAmountKeywords || "");
|
||||
setSubscriptionKeywords(settings.subscriptionKeywords || "");
|
||||
setSftpHost(settings.sftpHost || "");
|
||||
setSftpPort(settings.sftpPort || 22);
|
||||
setSftpUsername(settings.sftpUsername || "");
|
||||
@@ -77,6 +79,7 @@ export default function Settings() {
|
||||
orderNumberKeywords,
|
||||
supplierKeywords,
|
||||
totalAmountKeywords,
|
||||
subscriptionKeywords,
|
||||
sftpHost,
|
||||
sftpPort,
|
||||
sftpUsername,
|
||||
@@ -205,6 +208,20 @@ export default function Settings() {
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="subscriptionKeywords">Abonnement</Label>
|
||||
<Textarea
|
||||
id="subscriptionKeywords"
|
||||
value={subscriptionKeywords}
|
||||
onChange={(e) => setSubscriptionKeywords(e.target.value)}
|
||||
placeholder="Abonnement, Subscription, Mensuel, Annuel, Recurring"
|
||||
rows={2}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Les factures contenant ces mots-clés seront automatiquement marquées comme abonnement
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
1
drizzle/0006_fresh_bromley.sql
Normal file
1
drizzle/0006_fresh_bromley.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE `invoices` ADD `isSubscription` int DEFAULT 0 NOT NULL;
|
||||
1
drizzle/0007_faulty_titanium_man.sql
Normal file
1
drizzle/0007_faulty_titanium_man.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE `userSettings` ADD `subscriptionKeywords` text;
|
||||
1096
drizzle/meta/0006_snapshot.json
Normal file
1096
drizzle/meta/0006_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
1103
drizzle/meta/0007_snapshot.json
Normal file
1103
drizzle/meta/0007_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -43,6 +43,20 @@
|
||||
"when": 1770819973462,
|
||||
"tag": "0005_small_roxanne_simpson",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 6,
|
||||
"version": "5",
|
||||
"when": 1770821197778,
|
||||
"tag": "0006_fresh_bromley",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 7,
|
||||
"version": "5",
|
||||
"when": 1770821233743,
|
||||
"tag": "0007_faulty_titanium_man",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -92,6 +92,9 @@ export const invoices = mysqlTable("invoices", {
|
||||
// Extracted text from PDF
|
||||
extractedText: text("extractedText"), // Full text extracted from the invoice PDF
|
||||
|
||||
// Subscription flag
|
||||
isSubscription: int("isSubscription").default(0).notNull(), // 0 = NON, 1 = OUI
|
||||
|
||||
// SFTP Export tracking
|
||||
exportedAt: timestamp("exportedAt"),
|
||||
exportMode: mysqlEnum("exportMode", ["manual", "automatic"]),
|
||||
@@ -122,6 +125,7 @@ export const userSettings = mysqlTable("userSettings", {
|
||||
orderNumberKeywords: text("orderNumberKeywords"), // Ex: "Cde client, Référence commande, PO Number"
|
||||
supplierKeywords: text("supplierKeywords"), // Ex: "Vendeur, Société, Émetteur"
|
||||
totalAmountKeywords: text("totalAmountKeywords"), // Ex: "Net à payer, Total à régler, Amount due"
|
||||
subscriptionKeywords: text("subscriptionKeywords"), // Ex: "Abonnement, Subscription, Mensuel, Annuel"
|
||||
// SFTP Configuration
|
||||
sftpHost: varchar("sftpHost", { length: 255 }),
|
||||
sftpPort: int("sftpPort").default(22),
|
||||
|
||||
@@ -74,6 +74,7 @@ async function processEmailAttachment(
|
||||
orderNumber: settings.orderNumberKeywords,
|
||||
supplier: settings.supplierKeywords,
|
||||
totalAmount: settings.totalAmountKeywords,
|
||||
subscription: settings.subscriptionKeywords,
|
||||
} : undefined;
|
||||
|
||||
const model = settings?.llmModel || "mistral-large-latest";
|
||||
|
||||
@@ -75,6 +75,7 @@ async function processFolderFile(
|
||||
orderNumber: settings.orderNumberKeywords,
|
||||
supplier: settings.supplierKeywords,
|
||||
totalAmount: settings.totalAmountKeywords,
|
||||
subscription: settings.subscriptionKeywords,
|
||||
} : undefined;
|
||||
|
||||
const model = settings?.llmModel || "mistral-large-latest";
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface ExtractedInvoiceData {
|
||||
pageRange: string;
|
||||
qualityScore: number; // 0-100
|
||||
extractedText: string | null; // Full text extracted from the invoice PDF
|
||||
isSubscription: boolean; // True if subscription keywords detected
|
||||
}
|
||||
|
||||
export interface MultiInvoiceResult {
|
||||
@@ -102,6 +103,7 @@ export async function extractInvoicesWithMistral(
|
||||
orderNumber?: string | null;
|
||||
supplier?: string | null;
|
||||
totalAmount?: string | null;
|
||||
subscription?: string | null;
|
||||
}
|
||||
): Promise<MultiInvoiceResult> {
|
||||
const startTime = Date.now();
|
||||
@@ -127,6 +129,7 @@ export async function extractInvoicesWithMistral(
|
||||
if (customKeywords.orderNumber) hints.push(`Numéro de commande: ${customKeywords.orderNumber}`);
|
||||
if (customKeywords.supplier) hints.push(`Fournisseur: ${customKeywords.supplier}`);
|
||||
if (customKeywords.totalAmount) hints.push(`Montant total: ${customKeywords.totalAmount}`);
|
||||
if (customKeywords.subscription) hints.push(`Abonnement: ${customKeywords.subscription}`);
|
||||
|
||||
if (hints.length > 0) {
|
||||
keywordsHint = `\n\nMots-clés personnalisés à rechercher:\n${hints.join("\n")}`;
|
||||
@@ -145,7 +148,8 @@ Pour chaque facture trouvée, extrais les informations suivantes:
|
||||
- totalAmount: Montant total TTC (nombre décimal)
|
||||
- pageRange: Plage de pages de cette facture (ex: "1-2" ou "5")
|
||||
- qualityScore: Score de qualité de l'extraction de 0 à 100 (100 = toutes les informations trouvées et claires)
|
||||
- extractedText: Texte complet extrait de la facture (tout le texte visible sur les pages de cette facture)${keywordsHint}
|
||||
- extractedText: Texte complet extrait de la facture (tout le texte visible sur les pages de cette facture)
|
||||
- isSubscription: true si la facture contient des mots-clés liés à un abonnement (abonnement, subscription, mensuel, annuel, recurring, etc.), false sinon${keywordsHint}
|
||||
|
||||
Réponds UNIQUEMENT avec un objet JSON valide au format suivant:
|
||||
{
|
||||
@@ -161,7 +165,8 @@ Réponds UNIQUEMENT avec un objet JSON valide au format suivant:
|
||||
"totalAmount": 123.45,
|
||||
"pageRange": "1-2",
|
||||
"qualityScore": 85,
|
||||
"extractedText": "Texte complet de la facture..."
|
||||
"extractedText": "Texte complet de la facture...",
|
||||
"isSubscription": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -157,6 +157,7 @@ export const appRouter = router({
|
||||
orderNumber: settings.orderNumberKeywords,
|
||||
supplier: settings.supplierKeywords,
|
||||
totalAmount: settings.totalAmountKeywords,
|
||||
subscription: settings.subscriptionKeywords,
|
||||
} : undefined;
|
||||
|
||||
const model = settings?.llmModel || "mistral-large-latest";
|
||||
@@ -235,6 +236,7 @@ export const appRouter = router({
|
||||
pageRange: invoiceData.pageRange,
|
||||
qualityScore: invoiceData.qualityScore,
|
||||
extractedText: invoiceData.extractedText,
|
||||
isSubscription: invoiceData.isSubscription ? 1 : 0,
|
||||
metadataFileKey: metadataKey,
|
||||
metadataFileUrl: metadataUrl,
|
||||
status: "completed",
|
||||
@@ -308,6 +310,7 @@ export const appRouter = router({
|
||||
serviceConcerne: z.string().optional(),
|
||||
typeAchat: z.enum(["CAPEX", "OPEX"]).optional(),
|
||||
ventilationComptable: z.string().optional(),
|
||||
isSubscription: z.number().min(0).max(1).optional(),
|
||||
}),
|
||||
}))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
@@ -374,6 +377,7 @@ export const appRouter = router({
|
||||
orderNumberKeywords: z.string().optional(),
|
||||
supplierKeywords: z.string().optional(),
|
||||
totalAmountKeywords: z.string().optional(),
|
||||
subscriptionKeywords: z.string().optional(),
|
||||
sftpHost: z.string().optional(),
|
||||
sftpPort: z.number().optional(),
|
||||
sftpUsername: z.string().optional(),
|
||||
|
||||
12
todo.md
12
todo.md
@@ -247,3 +247,15 @@
|
||||
- [x] Ajouter un bouton "Détail" (icône FileText) dans la liste des factures
|
||||
- [x] Créer une boîte de dialogue pour afficher le texte complet
|
||||
- [ ] Tester l'affichage du texte extrait avec une nouvelle facture importée
|
||||
|
||||
## Champ Abonnement pour les factures
|
||||
- [x] Ajouter colonne isSubscription (OUI/NON) à la table invoices avec valeur par défaut NON
|
||||
- [x] Ajouter champ subscriptionKeywords dans la table userSettings
|
||||
- [x] Ajouter interface pour gérer les mots-clés d'abonnement dans la page Settings
|
||||
- [x] Modifier l'extraction pour détecter automatiquement les abonnements via mots-clés
|
||||
- [x] Modifier le prompt Mistral pour demander isSubscription
|
||||
- [x] Modifier routers.ts, emailImportService.ts et folderImportService.ts pour utiliser subscriptionKeywords
|
||||
- [x] Ajouter la colonne Abonnement dans la liste des factures
|
||||
- [x] Permettre l'édition directe du champ Abonnement dans la liste (select OUI/NON)
|
||||
- [x] Ajouter isSubscription dans le schéma de validation update
|
||||
- [ ] Tester la détection automatique avec une nouvelle facture importée
|
||||
|
||||
Reference in New Issue
Block a user