Nouvelle fonctionnalité majeure : ✅ Page "Automatismes" complète pour gérer les règles de remplissage automatique ✅ Système de règles conditionnelles SI...ALORS pour automatiser le remplissage des champs métier ✅ Support de conditions multiples avec logique AND/OR ✅ Application automatique des règles lors de l'import de factures ✅ Gestion de la priorité des règles (ordre d'exécution) ✅ Activation/désactivation des règles individuelles Architecture technique : - drizzle/schema.ts : Table automationRules avec conditions JSON et actions JSON - server/db.ts : Fonctions CRUD complètes pour les règles (create, update, delete, list) - server/routers.ts : Routes tRPC pour gérer les automatismes - server/automationEngine.ts : Moteur d'évaluation et d'application des règles - client/src/pages/AutomationRules.tsx : Interface complète de gestion des règles - Migration 0009_regular_warhawk.sql appliquée avec succès Fonctionnement des règles : 1. **Conditions (SI)** : Définissez des critères basés sur les champs de la facture - Champs disponibles : Fournisseur, N° Facture, Montant, Date, N° Commande, N° Bon de livraison - Opérateurs : contient, égal à, commence par, finit par, >, <, >=, <= - Logique : AND (toutes les conditions) ou OR (au moins une condition) 2. **Actions (ALORS)** : Définissez les champs à remplir automatiquement - Type d'achat (CAPEX/OPEX) - Service concerné (depuis la liste des services) - Ventilation comptable (depuis la liste des ventilations) 3. **Priorité** : Les règles sont appliquées dans l'ordre de priorité - Une règle de priorité supérieure ne peut pas être écrasée par une règle de priorité inférieure 4. **Application automatique** : Les règles s'appliquent automatiquement lors de l'import de factures Exemples d'utilisation : - SI Fournisseur contient "Microsoft" ET Montant > 1000 ALORS Type d'achat = CAPEX, Service = DSI - SI Fournisseur contient "EDF" ALORS Type d'achat = OPEX, Ventilation = TOUS - SI N° Facture commence par "AB" ALORS Service = Administration Interface utilisateur : - Liste des règles avec nom, conditions, actions et statut - Formulaire de création/édition avec interface intuitive - Boutons d'activation/désactivation rapide - Suppression avec confirmation - Affichage lisible des conditions et actions Cette fonctionnalité permet de gagner un temps considérable en automatisant le remplissage des champs métier selon des règles métier prédéfinies.
282 lines
14 KiB
TypeScript
282 lines
14 KiB
TypeScript
import { int, mysqlEnum, mysqlTable, text, timestamp, varchar, uniqueIndex, decimal } from "drizzle-orm/mysql-core";
|
|
|
|
/**
|
|
* Core user table backing auth flow.
|
|
* Supports multiple authentication methods: Manus OAuth, local, and Azure AD
|
|
*/
|
|
export const users = mysqlTable("users", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
/** Manus OAuth identifier (openId) - Optional for backward compatibility */
|
|
openId: varchar("openId", { length: 64 }).unique(),
|
|
/** Azure AD Object ID - Unique identifier from Azure AD */
|
|
azureAdId: varchar("azureAdId", { length: 64 }).unique(),
|
|
name: text("name"),
|
|
email: varchar("email", { length: 320 }).notNull().unique(),
|
|
/** Hashed password for local authentication (bcrypt) */
|
|
passwordHash: varchar("passwordHash", { length: 255 }),
|
|
/** Authentication method: 'manus', 'local', 'azure-ad' */
|
|
loginMethod: mysqlEnum("loginMethod", ["manus", "local", "azure-ad"]).notNull(),
|
|
role: mysqlEnum("role", ["user", "admin"]).default("user").notNull(),
|
|
/** Account status for manual user management */
|
|
isActive: int("isActive").default(1).notNull(), // 0 = inactive, 1 = active
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
|
lastSignedIn: timestamp("lastSignedIn").defaultNow().notNull(),
|
|
});
|
|
|
|
export type User = typeof users.$inferSelect;
|
|
export type InsertUser = typeof users.$inferInsert;
|
|
|
|
/**
|
|
* Source files table storing uploaded PDF files that may contain multiple invoices
|
|
*/
|
|
export const sourceFiles = mysqlTable("sourceFiles", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
userId: int("userId").notNull(),
|
|
fileName: varchar("fileName", { length: 255 }).notNull(),
|
|
fileKey: text("fileKey").notNull(), // Local storage key with YYYY-MM prefix
|
|
fileUrl: text("fileUrl").notNull(), // Public URL
|
|
totalInvoicesDetected: int("totalInvoicesDetected").default(0).notNull(),
|
|
processingStatus: mysqlEnum("processingStatus", ["processing", "completed", "error"]).default("processing").notNull(),
|
|
processingProgress: varchar("processingProgress", { length: 255 }), // Progress message (e.g., "Extraction 3/9 factures...")
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
|
});
|
|
|
|
export type SourceFile = typeof sourceFiles.$inferSelect;
|
|
export type InsertSourceFile = typeof sourceFiles.$inferInsert;
|
|
|
|
/**
|
|
* Invoices table storing individual invoices extracted from source files
|
|
*/
|
|
export const invoices = mysqlTable("invoices", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
userId: int("userId").notNull(),
|
|
sourceFileId: int("sourceFileId").notNull(), // Reference to the source PDF file
|
|
invoiceIndexInFile: int("invoiceIndexInFile").default(1).notNull(), // Position in the source file (1, 2, 3...)
|
|
|
|
// File storage information (for individual invoice if split, or reference to source)
|
|
fileName: varchar("fileName", { length: 255 }).notNull(),
|
|
fileKey: text("fileKey").notNull(), // Local storage key
|
|
fileUrl: text("fileUrl").notNull(), // Public URL
|
|
|
|
// Extracted metadata
|
|
supplierName: varchar("supplierName", { length: 255 }),
|
|
invoiceNumber: varchar("invoiceNumber", { length: 100 }),
|
|
invoiceDate: timestamp("invoiceDate"),
|
|
deliveryNoteNumber: varchar("deliveryNoteNumber", { length: 100 }),
|
|
orderNumber: varchar("orderNumber", { length: 100 }),
|
|
totalAmount: decimal("totalAmount", { precision: 10, scale: 2 }),
|
|
pageRange: varchar("pageRange", { length: 20 }), // ex: "1-2" ou "5"
|
|
qualityScore: int("qualityScore"), // Score de qualité de l'extraction (0-100)
|
|
|
|
// Metadata JSON file
|
|
metadataFileKey: text("metadataFileKey"), // Storage key for JSON metadata
|
|
metadataFileUrl: text("metadataFileUrl"), // Public URL for JSON
|
|
|
|
// Processing status
|
|
status: mysqlEnum("status", ["processing", "completed", "error"]).default("processing").notNull(),
|
|
errorMessage: text("errorMessage"),
|
|
|
|
// Export status
|
|
exportStatus: mysqlEnum("exportStatus", ["not_exported", "exported", "export_error"]).default("not_exported").notNull(),
|
|
|
|
// Manual correction tracking
|
|
manuallyEdited: int("manuallyEdited").default(0).notNull(), // 0 = false, 1 = true
|
|
|
|
// Business fields (optional)
|
|
serviceConcerne: varchar("serviceConcerne", { length: 100 }), // Service concerné (DSI, TRAVAUX, etc.)
|
|
typeAchat: mysqlEnum("typeAchat", ["CAPEX", "OPEX"]), // Type d'achat
|
|
ventilationComptable: varchar("ventilationComptable", { length: 100 }), // Ventilation comptable (TOUS, PA, HEP, etc.)
|
|
|
|
// 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"]),
|
|
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
|
}, (table) => {
|
|
return {
|
|
// Unique constraint: no duplicate invoices with same supplier, invoice number, and date
|
|
supplierInvoiceDateIdx: uniqueIndex("supplier_invoice_date_unique").on(table.supplierName, table.invoiceNumber, table.invoiceDate),
|
|
};
|
|
});
|
|
|
|
export type Invoice = typeof invoices.$inferSelect;
|
|
export type InsertInvoice = typeof invoices.$inferInsert;
|
|
|
|
/**
|
|
* User settings table for application preferences
|
|
*/
|
|
export const userSettings = mysqlTable("userSettings", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
userId: int("userId").notNull().unique(), // One settings record per user
|
|
llmModel: varchar("llmModel", { length: 50 }).default("mistral-large-latest").notNull(), // Mistral model for invoice extraction
|
|
orderNumberFormat: text("orderNumberFormat"), // Format/pattern du numéro de commande pour aider l'extraction
|
|
// Mots-clés personnalisés pour améliorer la détection (séparés par des virgules)
|
|
invoiceNumberKeywords: text("invoiceNumberKeywords"), // Ex: "Référence, Ref facture, Invoice ref"
|
|
deliveryNoteKeywords: text("deliveryNoteKeywords"), // Ex: "Livraison, Delivery, Expédition"
|
|
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),
|
|
sftpUsername: varchar("sftpUsername", { length: 255 }),
|
|
sftpPassword: text("sftpPassword"), // Encrypted password
|
|
sftpRemotePath: varchar("sftpRemotePath", { length: 500 }).default("/"), // Remote directory path
|
|
sftpAutoExport: int("sftpAutoExport").default(0).notNull(), // 0 = manual, 1 = automatic
|
|
// LLM Logs retention
|
|
llmLogsRetentionMonths: int("llmLogsRetentionMonths").default(3).notNull(), // Durée de conservation des logs LLM en mois (défaut: 3 mois)
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
|
});
|
|
|
|
export type UserSettings = typeof userSettings.$inferSelect;
|
|
export type InsertUserSettings = typeof userSettings.$inferInsert;
|
|
|
|
/**
|
|
* Import settings table for configuring different import methods
|
|
*/
|
|
export const importSettings = mysqlTable("importSettings", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
userId: int("userId").notNull().unique(), // One settings record per user
|
|
|
|
// Manual import settings
|
|
manualImportEnabled: int("manualImportEnabled").default(1).notNull(), // 0 = disabled, 1 = enabled
|
|
|
|
// Automatic import from folder settings
|
|
autoImportEnabled: int("autoImportEnabled").default(0).notNull(), // 0 = disabled, 1 = enabled
|
|
autoImportSourcePath: text("autoImportSourcePath"), // Path to folder to watch
|
|
autoImportFrequency: int("autoImportFrequency").default(60).notNull(), // Frequency in minutes (default: 60 = 1 hour)
|
|
|
|
// Email import settings
|
|
emailImportEnabled: int("emailImportEnabled").default(0).notNull(), // 0 = disabled, 1 = enabled
|
|
emailImportAddress: varchar("emailImportAddress", { length: 320 }), // Email address to monitor
|
|
emailImportPassword: text("emailImportPassword"), // Encrypted password
|
|
emailImportHost: varchar("emailImportHost", { length: 255 }), // IMAP server host
|
|
emailImportPort: int("emailImportPort").default(993), // IMAP port (default: 993 for SSL)
|
|
emailImportFrequency: int("emailImportFrequency").default(30).notNull(), // Frequency in minutes (default: 30)
|
|
|
|
// Export folder settings
|
|
exportFolder: text("exportFolder"), // Path to folder for exporting invoices
|
|
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
|
});
|
|
|
|
export type ImportSettings = typeof importSettings.$inferSelect;
|
|
export type InsertImportSettings = typeof importSettings.$inferInsert;
|
|
|
|
/**
|
|
* Import logs table for tracking all import operations
|
|
*/
|
|
export const importLogs = mysqlTable("importLogs", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
userId: int("userId").notNull(),
|
|
sourceFileId: int("sourceFileId").notNull(), // Reference to source file
|
|
fileName: varchar("fileName", { length: 255 }).notNull(),
|
|
totalInvoicesDetected: int("totalInvoicesDetected").default(0).notNull(),
|
|
invoicesImported: int("invoicesImported").default(0).notNull(),
|
|
duplicatesIgnored: int("duplicatesIgnored").default(0).notNull(),
|
|
errors: int("errors").default(0).notNull(),
|
|
duplicateDetails: text("duplicateDetails"), // JSON array of duplicate invoice info
|
|
errorDetails: text("errorDetails"), // JSON array of error messages
|
|
importedAt: timestamp("importedAt").defaultNow().notNull(),
|
|
});
|
|
|
|
export type ImportLog = typeof importLogs.$inferSelect;
|
|
export type InsertImportLog = typeof importLogs.$inferInsert;
|
|
|
|
/**
|
|
* LLM Logs table for storing raw LLM responses for debugging and improvement
|
|
*/
|
|
export const llmLogs = mysqlTable("llmLogs", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
userId: int("userId").notNull(),
|
|
sourceFileId: int("sourceFileId"), // Optional: link to source file if applicable
|
|
invoiceId: int("invoiceId"), // Optional: link to invoice if applicable
|
|
operation: varchar("operation", { length: 50 }).notNull(), // "detection" or "extraction"
|
|
model: varchar("model", { length: 50 }).notNull(), // LLM model used
|
|
promptSent: text("promptSent").notNull(), // Full prompt sent to LLM
|
|
rawResponse: text("rawResponse").notNull(), // Raw response from LLM (before cleaning)
|
|
cleanedResponse: text("cleanedResponse"), // Response after markdown cleaning
|
|
success: int("success").default(1).notNull(), // 1 = success, 0 = error
|
|
errorMessage: text("errorMessage"), // Error message if failed
|
|
processingTimeMs: int("processingTimeMs"), // Processing time in milliseconds
|
|
pageRange: varchar("pageRange", { length: 50 }), // Page range for this log (e.g., "1-2")
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
});
|
|
|
|
export type LlmLog = typeof llmLogs.$inferSelect;
|
|
export type InsertLlmLog = typeof llmLogs.$inferInsert;
|
|
|
|
/**
|
|
* Department list table for managing "Service concerné" values
|
|
*/
|
|
export const departmentList = mysqlTable("departmentList", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
userId: int("userId").notNull(), // Each user has their own list
|
|
name: varchar("name", { length: 100 }).notNull(),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
}, (table) => {
|
|
return {
|
|
// Unique constraint: no duplicate department names for the same user
|
|
userDepartmentIdx: uniqueIndex("user_department_unique").on(table.userId, table.name),
|
|
};
|
|
});
|
|
|
|
export type Department = typeof departmentList.$inferSelect;
|
|
export type InsertDepartment = typeof departmentList.$inferInsert;
|
|
|
|
/**
|
|
* Accounting allocation list table for managing "Ventilation comptable" values
|
|
*/
|
|
export const accountingAllocationList = mysqlTable("accountingAllocationList", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
userId: int("userId").notNull(), // Each user has their own list
|
|
name: varchar("name", { length: 100 }).notNull(),
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
}, (table) => {
|
|
return {
|
|
// Unique constraint: no duplicate allocation names for the same user
|
|
userAllocationIdx: uniqueIndex("user_allocation_unique").on(table.userId, table.name),
|
|
};
|
|
});
|
|
|
|
export type AccountingAllocation = typeof accountingAllocationList.$inferSelect;
|
|
export type InsertAccountingAllocation = typeof accountingAllocationList.$inferInsert;
|
|
|
|
/**
|
|
* Automation rules table for automatic field filling based on conditions
|
|
*/
|
|
export const automationRules = mysqlTable("automationRules", {
|
|
id: int("id").autoincrement().primaryKey(),
|
|
userId: int("userId").notNull(), // Each user has their own rules
|
|
name: varchar("name", { length: 255 }).notNull(), // Rule name for identification
|
|
isActive: int("isActive").default(1).notNull(), // 0 = disabled, 1 = enabled
|
|
priority: int("priority").default(0).notNull(), // Execution order (lower = higher priority)
|
|
|
|
// Conditions (IF) - JSON array of condition objects
|
|
// Example: [{"field": "supplierName", "operator": "contains", "value": "Microsoft"}, {"field": "totalAmount", "operator": ">", "value": "1000"}]
|
|
conditions: text("conditions").notNull(), // JSON string
|
|
conditionsLogic: mysqlEnum("conditionsLogic", ["AND", "OR"]).default("AND").notNull(), // How to combine conditions
|
|
|
|
// Actions (THEN) - JSON object with field assignments
|
|
// Example: {"typeAchat": "CAPEX", "serviceConcerne": "DSI", "ventilationComptable": "TOUS"}
|
|
actions: text("actions").notNull(), // JSON string
|
|
|
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
|
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
|
});
|
|
|
|
export type AutomationRule = typeof automationRules.$inferSelect;
|
|
export type InsertAutomationRule = typeof automationRules.$inferInsert;
|