Merge pull request #166 from R0m1k3/1.1

Allow invoice verification using delivery's bill of lading number
This commit is contained in:
LogiFlow authored and GitHub committed 2025-08-16 09:01:12 +02:00
commit 3655c71f49
3 files changed
+379 -34

No files matched your search

+27 -8
View File
@@ -135,10 +135,14 @@ export default function BLReconciliation() {
// Fonction pour déclencher la vérification // Fonction pour déclencher la vérification
const handleVerifyInvoice = (delivery: any, forceRefresh: boolean = false) => { 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({ toast({
title: "Référence manquante", 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", variant: "destructive",
}); });
return; return;
@@ -152,6 +156,15 @@ export default function BLReconciliation() {
}); });
return; 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)); 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 // Fonction pour vérifier toutes les factures avec un bouton
const handleVerifyAllInvoices = () => { const handleVerifyAllInvoices = () => {
const deliveriesToVerify = manualReconciliationDeliveries.filter(delivery => const deliveriesToVerify = manualReconciliationDeliveries.filter(delivery =>
delivery.invoiceReference?.trim() && (delivery.invoiceReference?.trim() || delivery.blNumber?.trim()) &&
(delivery.group?.nocodbTableName || delivery.group?.nocodbConfigId || delivery.group?.webhookUrl) (delivery.group?.nocodbTableName || delivery.group?.nocodbConfigId || delivery.group?.webhookUrl)
); );
if (deliveriesToVerify.length === 0) { if (deliveriesToVerify.length === 0) {
toast({ toast({
title: "Aucune facture à vérifier", 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; return;
} }
@@ -187,7 +200,7 @@ export default function BLReconciliation() {
toast({ toast({
title: "Vérification lancée", 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) => { deliveriesWithBL.forEach((delivery: any) => {
// Vérifier seulement si on a une référence de facture et pas déjà de résultat // Vérifier seulement si on a une référence de facture ou un BL et pas déjà de résultat
if (delivery.invoiceReference && !verificationResults[delivery.id] && !verifyingDeliveries.has(delivery.id)) { const hasVerifiableData = delivery.invoiceReference || delivery.blNumber;
const notAlreadyProcessed = !verificationResults[delivery.id] && !verifyingDeliveries.has(delivery.id);
if (hasVerifiableData && notAlreadyProcessed) {
if (import.meta.env.DEV) { 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 // Délai pour éviter de surcharger le serveur - le cache évitera les appels inutiles
+323 -17
View File
@@ -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> { async verifyInvoiceByBL(blNumber: string, supplierName: string, groupId: number, forceRefresh: boolean = false): Promise<{
console.log('🔍 Recherche par BL:', { blNumber, groupId }); exists: boolean;
matchType: 'invoice_reference' | 'bl_number' | 'none';
// Pour le développement, simuler une recherche errorMessage?: string;
if (process.env.NODE_ENV === 'development') { invoiceReference?: string;
if (blNumber && blNumber.trim()) { 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 { return {
found: true, exists: false,
data: { matchType: 'none',
invoiceReference: `FACT_${blNumber}`, errorMessage: 'Numéro BL vide'
amount: 156.78,
supplier: 'Fournisseur BL'
}
}; };
} }
// 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 { const result = await this.verifyInvoiceByBL(blNumber, '', groupId, false);
found: false,
error: 'BL non trouvé' 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
View File
@@ -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) { if (!delivery.supplier || !delivery.group) {
console.log('❌ Livraison manque informations:', { 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" }); return res.status(400).json({ message: "Delivery missing supplier or group information" });
} }
if (!invoiceReference || !invoiceReference.trim()) { // Accepter soit une référence de facture soit un numéro de BL
return res.status(400).json({ message: "Référence de facture requise" }); 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:', { console.log('🔍 Vérification facture:', {
deliveryId, deliveryId,
invoiceReference, invoiceReference,
blNumber,
supplier: delivery.supplier?.name, supplier: delivery.supplier?.name,
group: delivery.group?.name, group: delivery.group?.name,
groupId: delivery.groupId groupId: delivery.groupId
}); });
// Appeler le service de vérification let result;
const result = await invoiceVerificationService.verifyInvoice(
invoiceReference, if (invoiceReference && invoiceReference.trim()) {
delivery.groupId, // Vérifier par référence de facture
forceRefresh || false 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); console.log('✅ Résultat vérification:', result);
res.json(result); res.json(result);