Checkpoint: Ajout complet du champ "Destinataire" (recipientName) dans toute l'application : schéma DB (invoices.recipientName + userSettings.recipientKeywords), extraction IA (prompt LLM + interface), services d'import (email, dossier, upload manuel), interface utilisateur (liste des factures, détail, paramètres mots-clés), règles d'automatisme et configuration des champs LLM obligatoires.
This commit is contained in:
@@ -237,6 +237,7 @@ export default function AutomationRules() {
|
||||
|
||||
const fieldOptions = [
|
||||
{ value: "supplierName", label: "Fournisseur" },
|
||||
{ value: "recipientName", label: "Destinataire" },
|
||||
{ value: "invoiceNumber", label: "N° Facture" },
|
||||
{ value: "totalAmount", label: "Montant" },
|
||||
{ value: "invoiceDate", label: "Date facture" },
|
||||
|
||||
@@ -28,6 +28,7 @@ export default function InvoiceDetail() {
|
||||
deliveryNoteNumber: "",
|
||||
orderNumber: "",
|
||||
totalAmount: "",
|
||||
recipientName: "",
|
||||
serviceConcerne: "",
|
||||
typeAchat: "",
|
||||
ventilationComptable: "",
|
||||
@@ -45,6 +46,7 @@ export default function InvoiceDetail() {
|
||||
deliveryNoteNumber: invoice.deliveryNoteNumber || "",
|
||||
orderNumber: invoice.orderNumber || "",
|
||||
totalAmount: invoice.totalAmount ? invoice.totalAmount.toString() : "",
|
||||
recipientName: (invoice as any).recipientName || "",
|
||||
serviceConcerne: invoice.serviceConcerne || "",
|
||||
typeAchat: invoice.typeAchat || "",
|
||||
ventilationComptable: invoice.ventilationComptable || "",
|
||||
@@ -87,6 +89,7 @@ export default function InvoiceDetail() {
|
||||
deliveryNoteNumber: formData.deliveryNoteNumber || undefined,
|
||||
orderNumber: formData.orderNumber || undefined,
|
||||
totalAmount: formData.totalAmount ? formData.totalAmount : undefined,
|
||||
recipientName: formData.recipientName || undefined,
|
||||
serviceConcerne: formData.serviceConcerne || undefined,
|
||||
typeAchat: (formData.typeAchat as "CAPEX" | "OPEX" | "") || undefined,
|
||||
ventilationComptable: formData.ventilationComptable || undefined,
|
||||
@@ -330,6 +333,20 @@ export default function InvoiceDetail() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="recipientName">Destinataire</Label>
|
||||
{isEditing ? (
|
||||
<Input
|
||||
id="recipientName"
|
||||
value={formData.recipientName}
|
||||
onChange={(e) => setFormData({ ...formData, recipientName: e.target.value })}
|
||||
placeholder="Nom du destinataire"
|
||||
/>
|
||||
) : (
|
||||
<div className="text-sm mt-1">{(invoice as any).recipientName || "-"}</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
@@ -60,15 +60,16 @@ export default function Invoices() {
|
||||
// Generate Excel file
|
||||
const worksheet = XLSX.utils.json_to_sheet(data.invoices.map(inv => ({
|
||||
'Fournisseur': inv.supplierName,
|
||||
'N\u00b0 Facture': inv.invoiceNumber,
|
||||
'Destinataire': (inv as any).recipientName,
|
||||
'N° Facture': inv.invoiceNumber,
|
||||
'Date': inv.invoiceDate,
|
||||
'N\u00b0 Bon de livraison': inv.deliveryNoteNumber,
|
||||
'N\u00b0 Commande': inv.orderNumber,
|
||||
'N° Bon de livraison': inv.deliveryNoteNumber,
|
||||
'N° Commande': inv.orderNumber,
|
||||
'Montant': inv.totalAmount,
|
||||
'Score': inv.qualityScore,
|
||||
'Statut export': inv.exportStatus,
|
||||
'Export\u00e9 le': inv.exportedAt,
|
||||
'Cr\u00e9\u00e9 le': inv.createdAt,
|
||||
'Exporté le': inv.exportedAt,
|
||||
'Créé le': inv.createdAt,
|
||||
})));
|
||||
|
||||
const workbook = XLSX.utils.book_new();
|
||||
@@ -398,6 +399,7 @@ export default function Invoices() {
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead>Fournisseur</TableHead>
|
||||
<TableHead>Destinataire</TableHead>
|
||||
<TableHead>N° Facture</TableHead>
|
||||
<TableHead>Date</TableHead>
|
||||
<TableHead>Montant</TableHead>
|
||||
@@ -426,6 +428,7 @@ export default function Invoices() {
|
||||
{invoice.supplierName || "Inconnu"}
|
||||
</button>
|
||||
</TableCell>
|
||||
<TableCell>{(invoice as any).recipientName || "-"}</TableCell>
|
||||
<TableCell>{invoice.invoiceNumber || "-"}</TableCell>
|
||||
<TableCell>
|
||||
{invoice.invoiceDate
|
||||
|
||||
@@ -664,6 +664,7 @@ export default function Settings() {
|
||||
const [supplierKeywords, setSupplierKeywords] = useState("");
|
||||
const [totalAmountKeywords, setTotalAmountKeywords] = useState("");
|
||||
const [subscriptionKeywords, setSubscriptionKeywords] = useState("");
|
||||
const [recipientKeywords, setRecipientKeywords] = useState("");
|
||||
const [sftpHost, setSftpHost] = useState("");
|
||||
const [sftpPort, setSftpPort] = useState(22);
|
||||
const [sftpUsername, setSftpUsername] = useState("");
|
||||
@@ -681,6 +682,7 @@ export default function Settings() {
|
||||
setSupplierKeywords(settings.supplierKeywords || "");
|
||||
setTotalAmountKeywords(settings.totalAmountKeywords || "");
|
||||
setSubscriptionKeywords(settings.subscriptionKeywords || "");
|
||||
setRecipientKeywords((settings as any).recipientKeywords || "");
|
||||
setSftpHost(settings.sftpHost || "");
|
||||
setSftpPort(settings.sftpPort || 22);
|
||||
setSftpUsername(settings.sftpUsername || "");
|
||||
@@ -723,6 +725,7 @@ export default function Settings() {
|
||||
supplierKeywords,
|
||||
totalAmountKeywords,
|
||||
subscriptionKeywords,
|
||||
recipientKeywords,
|
||||
sftpHost,
|
||||
sftpPort,
|
||||
sftpUsername,
|
||||
@@ -986,6 +989,24 @@ export default function Settings() {
|
||||
Les factures contenant ces mots-clés seront automatiquement marquées comme abonnement
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor="recipientKeywords" className="text-base font-semibold">
|
||||
Destinataire
|
||||
</Label>
|
||||
<Textarea
|
||||
id="recipientKeywords"
|
||||
value={recipientKeywords}
|
||||
onChange={(e) => setRecipientKeywords(e.target.value)}
|
||||
placeholder="Destinataire, Client, Adressé à, Bill to, Ship to"
|
||||
rows={2}
|
||||
className="resize-none"
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground flex items-center gap-2">
|
||||
<AlertCircle className="w-4 h-4" />
|
||||
Mots-clés pour identifier le destinataire/client de la facture
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
2
drizzle/0015_cool_redwing.sql
Normal file
2
drizzle/0015_cool_redwing.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE `invoices` ADD `recipientName` varchar(255);--> statement-breakpoint
|
||||
ALTER TABLE `userSettings` ADD `recipientKeywords` text;
|
||||
1464
drizzle/meta/0015_snapshot.json
Normal file
1464
drizzle/meta/0015_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -106,6 +106,13 @@
|
||||
"when": 1773530695041,
|
||||
"tag": "0014_watery_talos",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 15,
|
||||
"version": "5",
|
||||
"when": 1775984357982,
|
||||
"tag": "0015_cool_redwing",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -67,6 +67,7 @@ export const invoices = mysqlTable("invoices", {
|
||||
deliveryNoteNumber: varchar("deliveryNoteNumber", { length: 100 }),
|
||||
orderNumber: varchar("orderNumber", { length: 100 }),
|
||||
totalAmount: decimal("totalAmount", { precision: 10, scale: 2 }),
|
||||
recipientName: varchar("recipientName", { length: 255 }), // Destinataire de la facture
|
||||
pageRange: varchar("pageRange", { length: 20 }), // ex: "1-2" ou "5"
|
||||
qualityScore: int("qualityScore"), // Score de qualité de l'extraction (0-100)
|
||||
|
||||
@@ -134,6 +135,7 @@ export const userSettings = mysqlTable("userSettings", {
|
||||
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"
|
||||
recipientKeywords: text("recipientKeywords"), // Ex: "Destinataire, À l'attention de, Client"
|
||||
// SFTP Configuration
|
||||
sftpHost: varchar("sftpHost", { length: 255 }),
|
||||
sftpPort: int("sftpPort").default(22),
|
||||
|
||||
@@ -658,6 +658,7 @@ export async function initializeDefaultLlmFields(userId: number): Promise<void>
|
||||
{ fieldName: "totalAmount", displayName: "Montant total TTC", isRequired: 1, displayOrder: 4 },
|
||||
{ fieldName: "deliveryNoteNumber", displayName: "Numéro de bon de livraison", isRequired: 0, displayOrder: 5 },
|
||||
{ fieldName: "orderNumber", displayName: "Numéro de commande", isRequired: 0, displayOrder: 6 },
|
||||
{ fieldName: "recipientName", displayName: "Destinataire", isRequired: 0, displayOrder: 7 },
|
||||
];
|
||||
|
||||
const existingFields = await db.select().from(llmFieldsConfig).where(eq(llmFieldsConfig.userId, userId));
|
||||
|
||||
@@ -75,6 +75,7 @@ async function processEmailAttachment(
|
||||
supplier: settings.supplierKeywords,
|
||||
totalAmount: settings.totalAmountKeywords,
|
||||
subscription: settings.subscriptionKeywords,
|
||||
recipient: settings.recipientKeywords,
|
||||
} : undefined;
|
||||
|
||||
const model = settings?.llmModel || "mistral-large-latest";
|
||||
@@ -153,6 +154,7 @@ async function processEmailAttachment(
|
||||
deliveryNoteNumber: invoiceData.deliveryNoteNumber,
|
||||
orderNumber: invoiceData.orderNumber,
|
||||
totalAmount: invoiceData.totalAmount?.toString(),
|
||||
recipientName: invoiceData.recipientName,
|
||||
pageRange: invoiceData.pageRange,
|
||||
qualityScore: invoiceData.qualityScore,
|
||||
extractedText: invoiceData.extractedText,
|
||||
|
||||
@@ -76,6 +76,7 @@ async function processFolderFile(
|
||||
supplier: settings.supplierKeywords,
|
||||
totalAmount: settings.totalAmountKeywords,
|
||||
subscription: settings.subscriptionKeywords,
|
||||
recipient: settings.recipientKeywords,
|
||||
} : undefined;
|
||||
|
||||
const model = settings?.llmModel || "mistral-large-latest";
|
||||
@@ -154,6 +155,7 @@ async function processFolderFile(
|
||||
deliveryNoteNumber: invoiceData.deliveryNoteNumber,
|
||||
orderNumber: invoiceData.orderNumber,
|
||||
totalAmount: invoiceData.totalAmount?.toString(),
|
||||
recipientName: invoiceData.recipientName,
|
||||
pageRange: invoiceData.pageRange,
|
||||
qualityScore: invoiceData.qualityScore,
|
||||
extractedText: invoiceData.extractedText,
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface ExtractedInvoiceData {
|
||||
deliveryNoteNumber: string | null;
|
||||
orderNumber: string | null;
|
||||
totalAmount: number | null;
|
||||
recipientName: string | null; // Destinataire de la facture
|
||||
pageRange: string;
|
||||
qualityScore: number; // 0-100
|
||||
extractedText: string | null; // Full text extracted from the invoice PDF
|
||||
@@ -141,6 +142,7 @@ export async function extractInvoicesWithMistral(
|
||||
supplier?: string | null;
|
||||
totalAmount?: string | null;
|
||||
subscription?: string | null;
|
||||
recipient?: string | null;
|
||||
}
|
||||
): Promise<MultiInvoiceResult> {
|
||||
// Load user's field configuration
|
||||
@@ -184,6 +186,7 @@ export async function extractInvoicesWithMistral(
|
||||
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 (customKeywords.recipient) hints.push(`Destinataire: ${customKeywords.recipient}`);
|
||||
|
||||
if (hints.length > 0) {
|
||||
keywordsHint = `\n\nMots-clés personnalisés à rechercher:\n${hints.join("\n")}`;
|
||||
@@ -200,12 +203,13 @@ export async function extractInvoicesWithMistral(
|
||||
const prompt = `Tu es un expert en extraction de données de factures. Analyse ce document PDF et extrais toutes les factures qu'il contient.
|
||||
|
||||
Pour chaque facture trouvée, extrais les informations suivantes:
|
||||
- supplierName: Nom du fournisseur/vendeur
|
||||
- supplierName: Nom du fournisseur/vendeur (émetteur de la facture)
|
||||
- invoiceNumber: Numéro de la facture
|
||||
- invoiceDate: Date de la facture (format ISO 8601: YYYY-MM-DD)
|
||||
- deliveryNoteNumber: Numéro du bon de livraison (si présent)
|
||||
- orderNumber: Numéro de commande client (si présent)
|
||||
- totalAmount: Montant total TTC (nombre décimal)
|
||||
- recipientName: Nom du destinataire/client (société ou personne à qui la facture est adressée)
|
||||
- pageRange: Plage de pages de cette facture (ex: "1-2" ou "5")
|
||||
${qualityScoreInstruction}
|
||||
- extractedText: Texte complet extrait de la facture (tout le texte visible sur les pages de cette facture)
|
||||
@@ -223,6 +227,7 @@ Réponds UNIQUEMENT avec un objet JSON valide au format suivant:
|
||||
"deliveryNoteNumber": "...",
|
||||
"orderNumber": "...",
|
||||
"totalAmount": 123.45,
|
||||
"recipientName": "...",
|
||||
"pageRange": "1-2",
|
||||
"qualityScore": 85,
|
||||
"extractedText": "Texte complet de la facture...",
|
||||
@@ -318,6 +323,7 @@ export function generateMetadataJSON(invoice: ExtractedInvoiceData): string {
|
||||
deliveryNoteNumber: invoice.deliveryNoteNumber,
|
||||
orderNumber: invoice.orderNumber,
|
||||
totalAmount: invoice.totalAmount,
|
||||
recipientName: invoice.recipientName,
|
||||
pageRange: invoice.pageRange,
|
||||
qualityScore: invoice.qualityScore,
|
||||
extractedAt: new Date().toISOString(),
|
||||
|
||||
@@ -185,6 +185,7 @@ export const appRouter = router({
|
||||
supplier: settings.supplierKeywords,
|
||||
totalAmount: settings.totalAmountKeywords,
|
||||
subscription: settings.subscriptionKeywords,
|
||||
recipient: settings.recipientKeywords,
|
||||
} : undefined;
|
||||
|
||||
const model = settings?.llmModel || "mistral-large-latest";
|
||||
@@ -260,6 +261,7 @@ export const appRouter = router({
|
||||
deliveryNoteNumber: invoiceData.deliveryNoteNumber,
|
||||
orderNumber: invoiceData.orderNumber,
|
||||
totalAmount: invoiceData.totalAmount?.toString(),
|
||||
recipientName: invoiceData.recipientName,
|
||||
pageRange: invoiceData.pageRange,
|
||||
qualityScore: invoiceData.qualityScore,
|
||||
extractedText: invoiceData.extractedText,
|
||||
@@ -350,6 +352,7 @@ export const appRouter = router({
|
||||
deliveryNoteNumber: z.string().optional(),
|
||||
orderNumber: z.string().optional(),
|
||||
totalAmount: z.string().optional(),
|
||||
recipientName: z.string().optional(),
|
||||
serviceConcerne: z.string().optional(),
|
||||
typeAchat: z.enum(["CAPEX", "OPEX"]).optional(),
|
||||
ventilationComptable: z.string().optional(),
|
||||
@@ -459,6 +462,7 @@ export const appRouter = router({
|
||||
supplierKeywords: z.string().optional(),
|
||||
totalAmountKeywords: z.string().optional(),
|
||||
subscriptionKeywords: z.string().optional(),
|
||||
recipientKeywords: z.string().optional(),
|
||||
sftpHost: z.string().optional(),
|
||||
sftpPort: z.number().optional(),
|
||||
sftpUsername: z.string().optional(),
|
||||
|
||||
23
todo.md
23
todo.md
@@ -555,5 +555,26 @@
|
||||
- [x] Modifier la route tRPC `auth.loginLocal` pour accepter un username libre
|
||||
- [x] Modifier la page Login.tsx pour remplacer le champ email par un champ "Identifiant"
|
||||
- [x] Créer le compte admin `adminItinova` avec mot de passe `Itinova69!` en base de données locale
|
||||
- [ ] Créer le compte admin `adminItinova` sur le VPS
|
||||
- [x] Créer le compte admin `adminItinova` sur le VPS
|
||||
- [x] Déployer sur le VPS
|
||||
|
||||
## Correction connexion adminItinova VPS
|
||||
- [x] Diagnostiquer le problème d'authentification (hash bcrypt Python vs Node.js)
|
||||
- [x] Corriger le hash du mot de passe sur le VPS (généré avec Node.js bcrypt)
|
||||
- [x] Vérifier que la connexion fonctionne (API tRPC répond 200 avec user)
|
||||
|
||||
## Ajout du champ Destinataire
|
||||
- [x] Ajouter colonne `recipientName` dans la table `invoices` (schéma DB + migration)
|
||||
- [x] Ajouter colonne `recipientKeywords` dans la table `userSettings` (schéma DB + migration)
|
||||
- [x] Mettre à jour le prompt LLM pour extraire le destinataire
|
||||
- [x] Mettre à jour l'interface `ExtractedInvoiceData` dans invoiceExtractor.ts
|
||||
- [x] Mettre à jour les mots-clés personnalisés dans invoiceExtractor.ts
|
||||
- [x] Mettre à jour la route `settings.upsert` dans routers.ts
|
||||
- [x] Mettre à jour la route `invoices.update` dans routers.ts
|
||||
- [x] Ajouter le champ destinataire dans InvoiceDetail.tsx
|
||||
- [x] Ajouter le champ mots-clés destinataire dans Settings.tsx (onglet Mots-clés)
|
||||
- [x] Ajouter destinataire dans AutomationRules.tsx (conditions)
|
||||
- [x] Ajouter recipientName dans les champs LLM par défaut (db.ts)
|
||||
- [x] Ajouter colonne Destinataire dans la liste des factures (Invoices.tsx)
|
||||
- [x] Ajouter Destinataire dans l'export Excel
|
||||
- [ ] Déployer sur le VPS
|
||||
|
||||
Reference in New Issue
Block a user