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>Service</TableHead>
|
||||||
<TableHead>Type achat</TableHead>
|
<TableHead>Type achat</TableHead>
|
||||||
<TableHead>Ventilation</TableHead>
|
<TableHead>Ventilation</TableHead>
|
||||||
|
<TableHead>Abonnement</TableHead>
|
||||||
<TableHead>Score</TableHead>
|
<TableHead>Score</TableHead>
|
||||||
<TableHead>Statut</TableHead>
|
<TableHead>Statut</TableHead>
|
||||||
<TableHead className="w-32">Actions</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>
|
<option value="__ADD_NEW__" className="font-semibold text-blue-600">➜ Ajouter...</option>
|
||||||
</select>
|
</select>
|
||||||
</TableCell>
|
</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>{getQualityBadge(invoice.qualityScore)}</TableCell>
|
||||||
<TableCell>{getExportStatusBadge(invoice.exportStatus || "not_exported")}</TableCell>
|
<TableCell>{getExportStatusBadge(invoice.exportStatus || "not_exported")}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export default function Settings() {
|
|||||||
const [orderNumberKeywords, setOrderNumberKeywords] = useState("");
|
const [orderNumberKeywords, setOrderNumberKeywords] = useState("");
|
||||||
const [supplierKeywords, setSupplierKeywords] = useState("");
|
const [supplierKeywords, setSupplierKeywords] = useState("");
|
||||||
const [totalAmountKeywords, setTotalAmountKeywords] = useState("");
|
const [totalAmountKeywords, setTotalAmountKeywords] = useState("");
|
||||||
|
const [subscriptionKeywords, setSubscriptionKeywords] = useState("");
|
||||||
const [sftpHost, setSftpHost] = useState("");
|
const [sftpHost, setSftpHost] = useState("");
|
||||||
const [sftpPort, setSftpPort] = useState(22);
|
const [sftpPort, setSftpPort] = useState(22);
|
||||||
const [sftpUsername, setSftpUsername] = useState("");
|
const [sftpUsername, setSftpUsername] = useState("");
|
||||||
@@ -36,6 +37,7 @@ export default function Settings() {
|
|||||||
setOrderNumberKeywords(settings.orderNumberKeywords || "");
|
setOrderNumberKeywords(settings.orderNumberKeywords || "");
|
||||||
setSupplierKeywords(settings.supplierKeywords || "");
|
setSupplierKeywords(settings.supplierKeywords || "");
|
||||||
setTotalAmountKeywords(settings.totalAmountKeywords || "");
|
setTotalAmountKeywords(settings.totalAmountKeywords || "");
|
||||||
|
setSubscriptionKeywords(settings.subscriptionKeywords || "");
|
||||||
setSftpHost(settings.sftpHost || "");
|
setSftpHost(settings.sftpHost || "");
|
||||||
setSftpPort(settings.sftpPort || 22);
|
setSftpPort(settings.sftpPort || 22);
|
||||||
setSftpUsername(settings.sftpUsername || "");
|
setSftpUsername(settings.sftpUsername || "");
|
||||||
@@ -77,6 +79,7 @@ export default function Settings() {
|
|||||||
orderNumberKeywords,
|
orderNumberKeywords,
|
||||||
supplierKeywords,
|
supplierKeywords,
|
||||||
totalAmountKeywords,
|
totalAmountKeywords,
|
||||||
|
subscriptionKeywords,
|
||||||
sftpHost,
|
sftpHost,
|
||||||
sftpPort,
|
sftpPort,
|
||||||
sftpUsername,
|
sftpUsername,
|
||||||
@@ -205,6 +208,20 @@ export default function Settings() {
|
|||||||
rows={2}
|
rows={2}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</CardContent>
|
||||||
</Card>
|
</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,
|
"when": 1770819973462,
|
||||||
"tag": "0005_small_roxanne_simpson",
|
"tag": "0005_small_roxanne_simpson",
|
||||||
"breakpoints": true
|
"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
|
// Extracted text from PDF
|
||||||
extractedText: text("extractedText"), // Full text extracted from the invoice 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
|
// SFTP Export tracking
|
||||||
exportedAt: timestamp("exportedAt"),
|
exportedAt: timestamp("exportedAt"),
|
||||||
exportMode: mysqlEnum("exportMode", ["manual", "automatic"]),
|
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"
|
orderNumberKeywords: text("orderNumberKeywords"), // Ex: "Cde client, Référence commande, PO Number"
|
||||||
supplierKeywords: text("supplierKeywords"), // Ex: "Vendeur, Société, Émetteur"
|
supplierKeywords: text("supplierKeywords"), // Ex: "Vendeur, Société, Émetteur"
|
||||||
totalAmountKeywords: text("totalAmountKeywords"), // Ex: "Net à payer, Total à régler, Amount due"
|
totalAmountKeywords: text("totalAmountKeywords"), // Ex: "Net à payer, Total à régler, Amount due"
|
||||||
|
subscriptionKeywords: text("subscriptionKeywords"), // Ex: "Abonnement, Subscription, Mensuel, Annuel"
|
||||||
// SFTP Configuration
|
// SFTP Configuration
|
||||||
sftpHost: varchar("sftpHost", { length: 255 }),
|
sftpHost: varchar("sftpHost", { length: 255 }),
|
||||||
sftpPort: int("sftpPort").default(22),
|
sftpPort: int("sftpPort").default(22),
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ async function processEmailAttachment(
|
|||||||
orderNumber: settings.orderNumberKeywords,
|
orderNumber: settings.orderNumberKeywords,
|
||||||
supplier: settings.supplierKeywords,
|
supplier: settings.supplierKeywords,
|
||||||
totalAmount: settings.totalAmountKeywords,
|
totalAmount: settings.totalAmountKeywords,
|
||||||
|
subscription: settings.subscriptionKeywords,
|
||||||
} : undefined;
|
} : undefined;
|
||||||
|
|
||||||
const model = settings?.llmModel || "mistral-large-latest";
|
const model = settings?.llmModel || "mistral-large-latest";
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ async function processFolderFile(
|
|||||||
orderNumber: settings.orderNumberKeywords,
|
orderNumber: settings.orderNumberKeywords,
|
||||||
supplier: settings.supplierKeywords,
|
supplier: settings.supplierKeywords,
|
||||||
totalAmount: settings.totalAmountKeywords,
|
totalAmount: settings.totalAmountKeywords,
|
||||||
|
subscription: settings.subscriptionKeywords,
|
||||||
} : undefined;
|
} : undefined;
|
||||||
|
|
||||||
const model = settings?.llmModel || "mistral-large-latest";
|
const model = settings?.llmModel || "mistral-large-latest";
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export interface ExtractedInvoiceData {
|
|||||||
pageRange: string;
|
pageRange: string;
|
||||||
qualityScore: number; // 0-100
|
qualityScore: number; // 0-100
|
||||||
extractedText: string | null; // Full text extracted from the invoice PDF
|
extractedText: string | null; // Full text extracted from the invoice PDF
|
||||||
|
isSubscription: boolean; // True if subscription keywords detected
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MultiInvoiceResult {
|
export interface MultiInvoiceResult {
|
||||||
@@ -102,6 +103,7 @@ export async function extractInvoicesWithMistral(
|
|||||||
orderNumber?: string | null;
|
orderNumber?: string | null;
|
||||||
supplier?: string | null;
|
supplier?: string | null;
|
||||||
totalAmount?: string | null;
|
totalAmount?: string | null;
|
||||||
|
subscription?: string | null;
|
||||||
}
|
}
|
||||||
): Promise<MultiInvoiceResult> {
|
): Promise<MultiInvoiceResult> {
|
||||||
const startTime = Date.now();
|
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.orderNumber) hints.push(`Numéro de commande: ${customKeywords.orderNumber}`);
|
||||||
if (customKeywords.supplier) hints.push(`Fournisseur: ${customKeywords.supplier}`);
|
if (customKeywords.supplier) hints.push(`Fournisseur: ${customKeywords.supplier}`);
|
||||||
if (customKeywords.totalAmount) hints.push(`Montant total: ${customKeywords.totalAmount}`);
|
if (customKeywords.totalAmount) hints.push(`Montant total: ${customKeywords.totalAmount}`);
|
||||||
|
if (customKeywords.subscription) hints.push(`Abonnement: ${customKeywords.subscription}`);
|
||||||
|
|
||||||
if (hints.length > 0) {
|
if (hints.length > 0) {
|
||||||
keywordsHint = `\n\nMots-clés personnalisés à rechercher:\n${hints.join("\n")}`;
|
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)
|
- totalAmount: Montant total TTC (nombre décimal)
|
||||||
- pageRange: Plage de pages de cette facture (ex: "1-2" ou "5")
|
- 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)
|
- 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:
|
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,
|
"totalAmount": 123.45,
|
||||||
"pageRange": "1-2",
|
"pageRange": "1-2",
|
||||||
"qualityScore": 85,
|
"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,
|
orderNumber: settings.orderNumberKeywords,
|
||||||
supplier: settings.supplierKeywords,
|
supplier: settings.supplierKeywords,
|
||||||
totalAmount: settings.totalAmountKeywords,
|
totalAmount: settings.totalAmountKeywords,
|
||||||
|
subscription: settings.subscriptionKeywords,
|
||||||
} : undefined;
|
} : undefined;
|
||||||
|
|
||||||
const model = settings?.llmModel || "mistral-large-latest";
|
const model = settings?.llmModel || "mistral-large-latest";
|
||||||
@@ -235,6 +236,7 @@ export const appRouter = router({
|
|||||||
pageRange: invoiceData.pageRange,
|
pageRange: invoiceData.pageRange,
|
||||||
qualityScore: invoiceData.qualityScore,
|
qualityScore: invoiceData.qualityScore,
|
||||||
extractedText: invoiceData.extractedText,
|
extractedText: invoiceData.extractedText,
|
||||||
|
isSubscription: invoiceData.isSubscription ? 1 : 0,
|
||||||
metadataFileKey: metadataKey,
|
metadataFileKey: metadataKey,
|
||||||
metadataFileUrl: metadataUrl,
|
metadataFileUrl: metadataUrl,
|
||||||
status: "completed",
|
status: "completed",
|
||||||
@@ -308,6 +310,7 @@ export const appRouter = router({
|
|||||||
serviceConcerne: z.string().optional(),
|
serviceConcerne: z.string().optional(),
|
||||||
typeAchat: z.enum(["CAPEX", "OPEX"]).optional(),
|
typeAchat: z.enum(["CAPEX", "OPEX"]).optional(),
|
||||||
ventilationComptable: z.string().optional(),
|
ventilationComptable: z.string().optional(),
|
||||||
|
isSubscription: z.number().min(0).max(1).optional(),
|
||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
@@ -374,6 +377,7 @@ export const appRouter = router({
|
|||||||
orderNumberKeywords: z.string().optional(),
|
orderNumberKeywords: z.string().optional(),
|
||||||
supplierKeywords: z.string().optional(),
|
supplierKeywords: z.string().optional(),
|
||||||
totalAmountKeywords: z.string().optional(),
|
totalAmountKeywords: z.string().optional(),
|
||||||
|
subscriptionKeywords: z.string().optional(),
|
||||||
sftpHost: z.string().optional(),
|
sftpHost: z.string().optional(),
|
||||||
sftpPort: z.number().optional(),
|
sftpPort: z.number().optional(),
|
||||||
sftpUsername: z.string().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] 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
|
- [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
|
- [ ] 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