mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-11 17:27:31 +02:00
Allow invoice verification using delivery's bill of lading number
Update the invoice verification process to accept either a direct invoice reference or a bill of lading number. This change enables the system to correctly identify and process invoices when only the bill of lading information is available, ensuring that all relevant deliveries are reconciled. The backend service has been enhanced to search for invoices using the bill of lading and supplier name, with improved caching and error handling for missing configurations. Replit-Commit-Author: Agent Replit-Commit-Session-Id: a3de0820-9397-41b2-b000-7931ee9c29de Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/a3de0820-9397-41b2-b000-7931ee9c29de/AO4neXk
This commit is contained in:
1 parent
54bdac580c
commit
c6c9369fcb
3 files changed
+379
-34
No files matched your search
@@ -135,10 +135,14 @@ export default function BLReconciliation() {
|
||||
|
||||
// Fonction pour déclencher la vérification
|
||||
const handleVerifyInvoice = (delivery: any, forceRefresh: boolean = false) => {
|
||||
if (!delivery.invoiceReference?.trim()) {
|
||||
// Accepter soit une référence de facture soit un numéro BL
|
||||
const hasInvoiceRef = delivery.invoiceReference?.trim();
|
||||
const hasBlNumber = delivery.blNumber?.trim();
|
||||
|
||||
if (!hasInvoiceRef && !hasBlNumber) {
|
||||
toast({
|
||||
title: "Référence manquante",
|
||||
description: "Veuillez saisir une référence de facture avant la vérification",
|
||||
description: "Veuillez saisir une référence de facture ou un numéro BL avant la vérification",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
@@ -152,6 +156,15 @@ export default function BLReconciliation() {
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('🔍 Déclenchement vérification:', {
|
||||
deliveryId: delivery.id,
|
||||
hasInvoiceRef,
|
||||
hasBlNumber,
|
||||
invoiceReference: delivery.invoiceReference,
|
||||
blNumber: delivery.blNumber,
|
||||
supplier: delivery.supplier?.name
|
||||
});
|
||||
|
||||
setVerifyingDeliveries(prev => new Set(prev).add(delivery.id));
|
||||
|
||||
@@ -166,14 +179,14 @@ export default function BLReconciliation() {
|
||||
// Fonction pour vérifier toutes les factures avec un bouton
|
||||
const handleVerifyAllInvoices = () => {
|
||||
const deliveriesToVerify = manualReconciliationDeliveries.filter(delivery =>
|
||||
delivery.invoiceReference?.trim() &&
|
||||
(delivery.invoiceReference?.trim() || delivery.blNumber?.trim()) &&
|
||||
(delivery.group?.nocodbTableName || delivery.group?.nocodbConfigId || delivery.group?.webhookUrl)
|
||||
);
|
||||
|
||||
if (deliveriesToVerify.length === 0) {
|
||||
toast({
|
||||
title: "Aucune facture à vérifier",
|
||||
description: "Aucune livraison avec référence de facture trouvée",
|
||||
description: "Aucune livraison avec référence de facture ou numéro BL trouvée",
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -187,7 +200,7 @@ export default function BLReconciliation() {
|
||||
|
||||
toast({
|
||||
title: "Vérification lancée",
|
||||
description: `Vérification de ${deliveriesToVerify.length} facture(s) en cours...`,
|
||||
description: `Vérification de ${deliveriesToVerify.length} facture(s)/BL en cours...`,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -231,10 +244,16 @@ export default function BLReconciliation() {
|
||||
}
|
||||
|
||||
deliveriesWithBL.forEach((delivery: any) => {
|
||||
// Vérifier seulement si on a une référence de facture et pas déjà de résultat
|
||||
if (delivery.invoiceReference && !verificationResults[delivery.id] && !verifyingDeliveries.has(delivery.id)) {
|
||||
// Vérifier seulement si on a une référence de facture ou un BL et pas déjà de résultat
|
||||
const hasVerifiableData = delivery.invoiceReference || delivery.blNumber;
|
||||
const notAlreadyProcessed = !verificationResults[delivery.id] && !verifyingDeliveries.has(delivery.id);
|
||||
|
||||
if (hasVerifiableData && notAlreadyProcessed) {
|
||||
if (import.meta.env.DEV) {
|
||||
console.log(`🔍 Vérification auto pour livraison ${delivery.id}:`, delivery.invoiceReference);
|
||||
console.log(`🔍 Vérification auto pour livraison ${delivery.id}:`, {
|
||||
invoiceRef: delivery.invoiceReference,
|
||||
blNumber: delivery.blNumber
|
||||
});
|
||||
}
|
||||
|
||||
// Délai pour éviter de surcharger le serveur - le cache évitera les appels inutiles
|
||||
|
||||
+323
-17
@@ -414,29 +414,335 @@ export class InvoiceVerificationService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche par numéro de BL
|
||||
* Vérifie une facture par numéro de BL et nom de fournisseur
|
||||
*/
|
||||
async searchByBLNumber(blNumber: string, groupId: number): Promise<any> {
|
||||
console.log('🔍 Recherche par BL:', { blNumber, groupId });
|
||||
|
||||
// Pour le développement, simuler une recherche
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (blNumber && blNumber.trim()) {
|
||||
async verifyInvoiceByBL(blNumber: string, supplierName: string, groupId: number, forceRefresh: boolean = false): Promise<{
|
||||
exists: boolean;
|
||||
matchType: 'invoice_reference' | 'bl_number' | 'none';
|
||||
errorMessage?: string;
|
||||
invoiceReference?: string;
|
||||
invoiceAmount?: number;
|
||||
supplierName?: string;
|
||||
fromCache?: boolean;
|
||||
}> {
|
||||
try {
|
||||
console.log('🔍 Début vérification facture par BL:', { blNumber, supplierName, groupId, forceRefresh });
|
||||
|
||||
if (!blNumber || !blNumber.trim()) {
|
||||
return {
|
||||
found: true,
|
||||
data: {
|
||||
invoiceReference: `FACT_${blNumber}`,
|
||||
amount: 156.78,
|
||||
supplier: 'Fournisseur BL'
|
||||
}
|
||||
exists: false,
|
||||
matchType: 'none',
|
||||
errorMessage: 'Numéro BL vide'
|
||||
};
|
||||
}
|
||||
|
||||
// Générer une clé de cache pour le BL
|
||||
const cacheKey = `bl_${groupId}_${blNumber.trim().toLowerCase()}_${supplierName.toLowerCase()}`;
|
||||
|
||||
// Vérifier le cache d'abord (sauf si refresh forcé)
|
||||
if (!forceRefresh) {
|
||||
try {
|
||||
const cached = await storage.getInvoiceVerificationCache(cacheKey);
|
||||
if (cached && new Date() < new Date(cached.expiresAt)) {
|
||||
console.log('💾 Cache hit pour BL:', { blNumber, groupId });
|
||||
return {
|
||||
exists: cached.exists,
|
||||
matchType: cached.matchType as 'invoice_reference' | 'bl_number' | 'none',
|
||||
errorMessage: cached.errorMessage || undefined,
|
||||
invoiceReference: cached.invoiceReference || undefined,
|
||||
supplierName: cached.supplierName || undefined,
|
||||
fromCache: true
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Erreur lecture cache BL:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Récupérer la configuration du groupe
|
||||
const group = await storage.getGroup(groupId);
|
||||
if (!group) {
|
||||
return {
|
||||
exists: false,
|
||||
matchType: 'none',
|
||||
errorMessage: 'Groupe non trouvé'
|
||||
};
|
||||
}
|
||||
|
||||
console.log('🔧 Configuration groupe pour BL:', {
|
||||
groupName: group.name,
|
||||
hasNocodbConfig: !!group.nocodbConfigId,
|
||||
hasTableName: !!group.nocodbTableName,
|
||||
hasWebhook: !!group.webhookUrl,
|
||||
blColumnName: group.nocodbBlColumnName
|
||||
});
|
||||
|
||||
// Si pas de configuration NocoDB, retourner un résultat par défaut
|
||||
if (!group.nocodbConfigId && !group.nocodbTableName && !group.webhookUrl) {
|
||||
console.log('⚠️ Pas de configuration NocoDB pour ce groupe');
|
||||
return {
|
||||
exists: false,
|
||||
matchType: 'none',
|
||||
errorMessage: 'Configuration NocoDB manquante pour ce magasin'
|
||||
};
|
||||
}
|
||||
|
||||
// Pour le développement, simuler une vérification
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('🔧 Mode développement - simulation vérification BL');
|
||||
|
||||
if (blNumber && blNumber.trim()) {
|
||||
const result = {
|
||||
exists: true,
|
||||
matchType: 'bl_number' as const,
|
||||
invoiceReference: `FACT_${blNumber}`,
|
||||
invoiceAmount: 156.78,
|
||||
supplierName: supplierName
|
||||
};
|
||||
|
||||
// Sauvegarder en cache
|
||||
try {
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setHours(expiresAt.getHours() + 24);
|
||||
|
||||
await storage.saveInvoiceVerificationCache({
|
||||
cacheKey,
|
||||
groupId,
|
||||
invoiceReference: result.invoiceReference,
|
||||
supplierName: supplierName,
|
||||
exists: result.exists,
|
||||
matchType: result.matchType,
|
||||
errorMessage: null,
|
||||
cacheHit: false,
|
||||
apiCallTime: null,
|
||||
expiresAt
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ Erreur sauvegarde cache BL:', error);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return {
|
||||
exists: false,
|
||||
matchType: 'none',
|
||||
errorMessage: 'BL non trouvé (mode développement)'
|
||||
};
|
||||
}
|
||||
|
||||
// En production, faire l'appel réel à NocoDB
|
||||
console.log('🔍 Vérification BL NocoDB en production...');
|
||||
|
||||
try {
|
||||
// Récupérer la configuration NocoDB active
|
||||
const nocodbConfig = await storage.getActiveNocodbConfig();
|
||||
if (!nocodbConfig) {
|
||||
console.log('⚠️ Pas de configuration NocoDB active');
|
||||
return {
|
||||
exists: false,
|
||||
matchType: 'none',
|
||||
errorMessage: 'Configuration NocoDB non trouvée'
|
||||
};
|
||||
}
|
||||
|
||||
// Utiliser l'ID de table configuré dans le groupe
|
||||
const tableId = group.nocodbTableId || 'mrr733dfb8wtt9b';
|
||||
console.log('🔧 Utilisation table ID pour BL:', {
|
||||
groupTable: group.nocodbTableName,
|
||||
configuredId: group.nocodbTableId,
|
||||
resolvedId: tableId
|
||||
});
|
||||
|
||||
// Rechercher par numéro de BL
|
||||
const blColumnName = group.nocodbBlColumnName || 'Numero_BL';
|
||||
let matchResult = await this.searchInNocoDB(
|
||||
blNumber,
|
||||
blColumnName,
|
||||
nocodbConfig,
|
||||
tableId
|
||||
);
|
||||
|
||||
// Gérer les erreurs d'API spécifiques
|
||||
if (matchResult.error) {
|
||||
const result = {
|
||||
exists: false,
|
||||
matchType: 'none' as const,
|
||||
errorMessage: matchResult.error
|
||||
};
|
||||
|
||||
// Sauvegarder en cache même les erreurs
|
||||
try {
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setHours(expiresAt.getHours() + 24);
|
||||
|
||||
await storage.saveInvoiceVerificationCache({
|
||||
cacheKey,
|
||||
groupId,
|
||||
invoiceReference: blNumber,
|
||||
supplierName: supplierName,
|
||||
exists: false,
|
||||
matchType: 'none',
|
||||
errorMessage: result.errorMessage,
|
||||
cacheHit: false,
|
||||
apiCallTime: null,
|
||||
expiresAt
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ Erreur sauvegarde cache erreur BL:', error);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
if (matchResult.found) {
|
||||
// Vérifier que le fournisseur correspond
|
||||
const foundSupplier = matchResult.data[group.nocodbSupplierColumnName || 'supplier'] || '';
|
||||
const supplierMatch = foundSupplier.toLowerCase().includes(supplierName.toLowerCase()) ||
|
||||
supplierName.toLowerCase().includes(foundSupplier.toLowerCase());
|
||||
|
||||
if (!supplierMatch) {
|
||||
console.log('⚠️ Fournisseur ne correspond pas:', {
|
||||
expected: supplierName,
|
||||
found: foundSupplier
|
||||
});
|
||||
return {
|
||||
exists: false,
|
||||
matchType: 'none',
|
||||
errorMessage: `BL trouvé mais fournisseur différent: ${foundSupplier} vs ${supplierName}`
|
||||
};
|
||||
}
|
||||
|
||||
const result = {
|
||||
exists: true,
|
||||
matchType: 'bl_number' as const,
|
||||
invoiceReference: matchResult.data[group.invoiceColumnName || 'RefFacture'] || `BL_${blNumber}`,
|
||||
invoiceAmount: parseFloat(matchResult.data[group.nocodbAmountColumnName || 'amount'] || '0'),
|
||||
supplierName: foundSupplier || supplierName
|
||||
};
|
||||
|
||||
// Sauvegarder en cache si succès
|
||||
try {
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setHours(expiresAt.getHours() + 24);
|
||||
|
||||
await storage.saveInvoiceVerificationCache({
|
||||
cacheKey,
|
||||
groupId,
|
||||
invoiceReference: result.invoiceReference,
|
||||
supplierName: result.supplierName,
|
||||
exists: true,
|
||||
matchType: 'bl_number',
|
||||
errorMessage: null,
|
||||
cacheHit: false,
|
||||
apiCallTime: null,
|
||||
expiresAt
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ Erreur sauvegarde cache succès BL:', error);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Aucune correspondance trouvée
|
||||
const notFoundResult = {
|
||||
exists: false,
|
||||
matchType: 'none' as const,
|
||||
errorMessage: 'Aucun BL correspondant trouvé dans NocoDB'
|
||||
};
|
||||
|
||||
// Sauvegarder même les résultats "non trouvé"
|
||||
try {
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setHours(expiresAt.getHours() + 24);
|
||||
|
||||
await storage.saveInvoiceVerificationCache({
|
||||
cacheKey,
|
||||
groupId,
|
||||
invoiceReference: blNumber,
|
||||
supplierName: supplierName,
|
||||
exists: false,
|
||||
matchType: 'none',
|
||||
errorMessage: notFoundResult.errorMessage,
|
||||
cacheHit: false,
|
||||
apiCallTime: null,
|
||||
expiresAt
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ Erreur sauvegarde cache not found BL:', error);
|
||||
}
|
||||
|
||||
return notFoundResult;
|
||||
|
||||
} catch (error) {
|
||||
const errorMessage = `Erreur NocoDB BL: ${error instanceof Error ? error.message : 'Erreur inconnue'}`;
|
||||
|
||||
console.error('❌ Erreur lors de la vérification BL NocoDB:', error);
|
||||
|
||||
const errorResult = {
|
||||
exists: false,
|
||||
matchType: 'none' as const,
|
||||
errorMessage
|
||||
};
|
||||
|
||||
// Sauvegarder les erreurs en cache
|
||||
try {
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setHours(expiresAt.getHours() + 24);
|
||||
|
||||
await storage.saveInvoiceVerificationCache({
|
||||
cacheKey,
|
||||
groupId,
|
||||
invoiceReference: blNumber,
|
||||
supplierName: supplierName,
|
||||
exists: false,
|
||||
matchType: 'none',
|
||||
errorMessage: errorResult.errorMessage,
|
||||
cacheHit: false,
|
||||
apiCallTime: null,
|
||||
expiresAt
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ Erreur sauvegarde cache erreur BL:', error);
|
||||
}
|
||||
|
||||
return errorResult;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Erreur vérification facture par BL:', error);
|
||||
return {
|
||||
exists: false,
|
||||
matchType: 'none',
|
||||
errorMessage: error instanceof Error ? error.message : 'Erreur inconnue'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche par numéro de BL (méthode simplifiée pour compatibilité)
|
||||
*/
|
||||
async searchByBLNumber(blNumber: string, groupId: number): Promise<any> {
|
||||
console.log('🔍 Recherche par BL (legacy):', { blNumber, groupId });
|
||||
|
||||
return {
|
||||
found: false,
|
||||
error: 'BL non trouvé'
|
||||
};
|
||||
const result = await this.verifyInvoiceByBL(blNumber, '', groupId, false);
|
||||
|
||||
if (result.exists) {
|
||||
return {
|
||||
found: true,
|
||||
data: {
|
||||
invoiceReference: result.invoiceReference,
|
||||
amount: result.invoiceAmount,
|
||||
supplier: result.supplierName
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
found: false,
|
||||
error: result.errorMessage || 'BL non trouvé'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+29
-9
@@ -882,7 +882,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
}
|
||||
}
|
||||
|
||||
const { invoiceReference, forceRefresh } = req.body;
|
||||
const { invoiceReference, blNumber, forceRefresh } = req.body;
|
||||
|
||||
if (!delivery.supplier || !delivery.group) {
|
||||
console.log('❌ Livraison manque informations:', {
|
||||
@@ -893,24 +893,44 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
return res.status(400).json({ message: "Delivery missing supplier or group information" });
|
||||
}
|
||||
|
||||
if (!invoiceReference || !invoiceReference.trim()) {
|
||||
return res.status(400).json({ message: "Référence de facture requise" });
|
||||
// Accepter soit une référence de facture soit un numéro de BL
|
||||
if ((!invoiceReference || !invoiceReference.trim()) && (!blNumber || !blNumber.trim())) {
|
||||
return res.status(400).json({ message: "Référence de facture ou numéro BL requis" });
|
||||
}
|
||||
|
||||
console.log('🔍 Vérification facture:', {
|
||||
deliveryId,
|
||||
invoiceReference,
|
||||
blNumber,
|
||||
supplier: delivery.supplier?.name,
|
||||
group: delivery.group?.name,
|
||||
groupId: delivery.groupId
|
||||
});
|
||||
|
||||
// Appeler le service de vérification
|
||||
const result = await invoiceVerificationService.verifyInvoice(
|
||||
invoiceReference,
|
||||
delivery.groupId,
|
||||
forceRefresh || false
|
||||
);
|
||||
let result;
|
||||
|
||||
if (invoiceReference && invoiceReference.trim()) {
|
||||
// Vérifier par référence de facture
|
||||
result = await invoiceVerificationService.verifyInvoice(
|
||||
invoiceReference,
|
||||
delivery.groupId,
|
||||
forceRefresh || false
|
||||
);
|
||||
} else if (blNumber && blNumber.trim()) {
|
||||
// Vérifier par numéro BL
|
||||
result = await invoiceVerificationService.verifyInvoiceByBL(
|
||||
blNumber,
|
||||
delivery.supplier.name,
|
||||
delivery.groupId,
|
||||
forceRefresh || false
|
||||
);
|
||||
} else {
|
||||
result = {
|
||||
exists: false,
|
||||
matchType: 'none',
|
||||
errorMessage: 'Aucune référence de facture ou numéro BL fourni'
|
||||
};
|
||||
}
|
||||
|
||||
console.log('✅ Résultat vérification:', result);
|
||||
res.json(result);
|
||||
|
||||
Reference in new issue
Block a user