mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-23 06:58:19 +02:00
5 files changed
+249
-81
No files matched your search
@@ -98,6 +98,61 @@ export default function BLReconciliation() {
|
|||||||
[variables.deliveryId]: result
|
[variables.deliveryId]: result
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Toast de confirmation avec détails de la vérification
|
||||||
|
if (result.exists) {
|
||||||
|
// Facture trouvée - afficher les détails
|
||||||
|
// Formater la date d'échéance si présente
|
||||||
|
let dueDateText = '';
|
||||||
|
if (result.dueDate) {
|
||||||
|
try {
|
||||||
|
const date = new Date(result.dueDate);
|
||||||
|
dueDateText = date.toLocaleDateString('fr-FR', {
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
year: 'numeric'
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
dueDateText = result.dueDate; // Afficher tel quel si le format n'est pas reconnu
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formater le montant avec garde contre NaN
|
||||||
|
const amountText = result.invoiceAmount ?
|
||||||
|
(() => {
|
||||||
|
const amount = parseFloat(result.invoiceAmount);
|
||||||
|
return isNaN(amount) ? 'Format invalide' : `${amount.toFixed(2)}€`;
|
||||||
|
})() :
|
||||||
|
'Non disponible';
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "✅ Facture vérifiée avec succès",
|
||||||
|
description: (
|
||||||
|
<div className="space-y-1 text-sm">
|
||||||
|
<div><strong>Référence :</strong> {result.invoiceReference || 'Non disponible'}</div>
|
||||||
|
<div><strong>Montant :</strong> {amountText}</div>
|
||||||
|
{result.dueDate ? (
|
||||||
|
<div className="text-green-600 font-medium">
|
||||||
|
<strong>📅 Échéance :</strong> {dueDateText}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-orange-600 font-medium">
|
||||||
|
⚠️ Aucune date d'échéance trouvée
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
duration: 5000,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Facture non trouvée - afficher message d'erreur
|
||||||
|
toast({
|
||||||
|
title: "⚠️ Facture non trouvée",
|
||||||
|
description: result.errorMessage || 'La facture n\'a pas été trouvée dans la base de données',
|
||||||
|
variant: "destructive",
|
||||||
|
duration: 5000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Auto-remplissage si facture trouvée via BL
|
// Auto-remplissage si facture trouvée via BL
|
||||||
if (result.exists && result.matchType === 'bl_number' && result.invoiceReference) {
|
if (result.exists && result.matchType === 'bl_number' && result.invoiceReference) {
|
||||||
// Auto-remplir les champs dans la livraison via API
|
// Auto-remplir les champs dans la livraison via API
|
||||||
@@ -789,6 +844,9 @@ export default function BLReconciliation() {
|
|||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Montant Fact.
|
Montant Fact.
|
||||||
</th>
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Échéance
|
||||||
|
</th>
|
||||||
<th className="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th className="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Écart
|
Écart
|
||||||
</th>
|
</th>
|
||||||
@@ -907,6 +965,17 @@ export default function BLReconciliation() {
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div className="text-sm">
|
||||||
|
{delivery.dueDate ? (
|
||||||
|
<span className={delivery.reconciled !== true ? 'font-medium text-gray-900' : 'text-gray-600'}>
|
||||||
|
{safeFormat(delivery.dueDate, 'dd/MM/yyyy')}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-gray-400 italic text-xs">-</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-center">
|
<td className="px-6 py-4 whitespace-nowrap text-center">
|
||||||
{(() => {
|
{(() => {
|
||||||
const blAmount = delivery.blAmount ? parseFloat(delivery.blAmount) : 0;
|
const blAmount = delivery.blAmount ? parseFloat(delivery.blAmount) : 0;
|
||||||
@@ -1133,6 +1202,9 @@ export default function BLReconciliation() {
|
|||||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Montant Fact.
|
Montant Fact.
|
||||||
</th>
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Échéance
|
||||||
|
</th>
|
||||||
<th className="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th className="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Écart
|
Écart
|
||||||
</th>
|
</th>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ The platform is built with a clear separation of concerns, utilizing a modern fu
|
|||||||
- **Reporting:** Statistical reporting, including monthly summaries of deliveries.
|
- **Reporting:** Statistical reporting, including monthly summaries of deliveries.
|
||||||
- **UI Harmonization:** All detail modals across modules (orders, deliveries, calendar) use a unified component for consistent information display.
|
- **UI Harmonization:** All detail modals across modules (orders, deliveries, calendar) use a unified component for consistent information display.
|
||||||
- **Analyse Vente (Sales Analysis):** A dedicated module for admins and directors that displays a configurable external URL/IP in an iframe. Configuration is managed via a dedicated tab in the Utilities page, allowing administrators to set or update the sales analysis dashboard URL. The menu is visible only to admin and directeur roles, ensuring proper access control. Includes intelligent error handling with informative messages when the URL is not configured. Features a refresh button to force complete iframe reloads.
|
- **Analyse Vente (Sales Analysis):** A dedicated module for admins and directors that displays a configurable external URL/IP in an iframe. Configuration is managed via a dedicated tab in the Utilities page, allowing administrators to set or update the sales analysis dashboard URL. The menu is visible only to admin and directeur roles, ensuring proper access control. Includes intelligent error handling with informative messages when the URL is not configured. Features a refresh button to force complete iframe reloads.
|
||||||
- **Échéancier des Paiements (Payment Schedule):** A comprehensive payment deadline tracking system integrated with NocoDB. Displays monthly aggregations of supplier payment schedules with breakdown by payment method (Virement, Traite, Traite Magnétique, Chèque). Features include: role-based access (admin/director), group-based filtering with selectedStoreId, Zod validation for security, robust supplier name normalization (case/accent/whitespace handling), monthly statistics with payment method breakdowns, and configurable NocoDB column mapping for due dates. Requires configuration of nocodbDueDateColumnName in Groups settings and paymentMethod in Suppliers. **Security (November 2025)**: Implements strict authorization - directors can only access their assigned groups, preventing cross-group data access.
|
- **Échéancier des Paiements (Payment Schedule):** A comprehensive payment deadline tracking system with hybrid architecture for optimal performance. **Architecture Refactorization (November 2025):** Due dates are now stored locally in deliveries table (dueDate column) as the single source of truth, retrieved once during invoice verification from NocoDB and cached locally for fast queries. Features include: role-based access (admin/director), group-based filtering with selectedStoreId, intelligent fallback to NocoDB for historical deliveries without cached dates, automatic date normalization handling multiple formats (ISO, French DD/MM/YYYY, American MM/DD/YYYY), monthly aggregations with payment method breakdowns (Virement, Traite, Traite Magnétique, Chèque), Zod validation for security, and robust supplier name normalization. Requires configuration of nocodbDueDateColumnName in Groups settings and paymentMethod in Suppliers. **Data Flow:** Invoice verification → NocoDB (fetch dueDate) → deliveries (local storage) → payment-schedule (fast local reads with NocoDB fallback). **Security (November 2025)**: Implements strict authorization - directors can only access their assigned groups, preventing cross-group data access.
|
||||||
|
|
||||||
## External Dependencies
|
## External Dependencies
|
||||||
- **API Météo:** Visual Crossing API
|
- **API Météo:** Visual Crossing API
|
||||||
|
|||||||
@@ -242,6 +242,7 @@ export class InvoiceVerificationService {
|
|||||||
invoiceReference?: string;
|
invoiceReference?: string;
|
||||||
invoiceAmount?: number;
|
invoiceAmount?: number;
|
||||||
supplierName?: string;
|
supplierName?: string;
|
||||||
|
dueDate?: string | null;
|
||||||
fromCache?: boolean;
|
fromCache?: boolean;
|
||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
@@ -396,7 +397,8 @@ export class InvoiceVerificationService {
|
|||||||
matchType: 'invoice_reference' as const,
|
matchType: 'invoice_reference' as const,
|
||||||
invoiceReference: matchResult.data.invoice_reference || invoiceReference,
|
invoiceReference: matchResult.data.invoice_reference || invoiceReference,
|
||||||
invoiceAmount: parseFloat(matchResult.data[group.nocodbAmountColumnName || 'amount'] || '0'),
|
invoiceAmount: parseFloat(matchResult.data[group.nocodbAmountColumnName || 'amount'] || '0'),
|
||||||
supplierName: matchResult.data[group.nocodbSupplierColumnName || 'supplier'] || 'Inconnu'
|
supplierName: matchResult.data[group.nocodbSupplierColumnName || 'supplier'] || 'Inconnu',
|
||||||
|
dueDate: matchResult.data[group.nocodbDueDateColumnName || 'Echeance'] || null
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sauvegarder en cache si succès
|
// Sauvegarder en cache si succès
|
||||||
@@ -432,7 +434,8 @@ export class InvoiceVerificationService {
|
|||||||
matchType: 'bl_number' as const,
|
matchType: 'bl_number' as const,
|
||||||
invoiceReference: matchResult.data.invoice_reference || `BL_${invoiceReference}`,
|
invoiceReference: matchResult.data.invoice_reference || `BL_${invoiceReference}`,
|
||||||
invoiceAmount: parseFloat(matchResult.data[group.nocodbAmountColumnName || 'amount'] || '0'),
|
invoiceAmount: parseFloat(matchResult.data[group.nocodbAmountColumnName || 'amount'] || '0'),
|
||||||
supplierName: matchResult.data[group.nocodbSupplierColumnName || 'supplier'] || 'Inconnu'
|
supplierName: matchResult.data[group.nocodbSupplierColumnName || 'supplier'] || 'Inconnu',
|
||||||
|
dueDate: matchResult.data[group.nocodbDueDateColumnName || 'Echeance'] || null
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sauvegarder en cache si succès
|
// Sauvegarder en cache si succès
|
||||||
@@ -599,6 +602,7 @@ export class InvoiceVerificationService {
|
|||||||
invoiceReference?: string;
|
invoiceReference?: string;
|
||||||
invoiceAmount?: number;
|
invoiceAmount?: number;
|
||||||
supplierName?: string;
|
supplierName?: string;
|
||||||
|
dueDate?: string | null;
|
||||||
fromCache?: boolean;
|
fromCache?: boolean;
|
||||||
}> {
|
}> {
|
||||||
try {
|
try {
|
||||||
@@ -795,7 +799,8 @@ export class InvoiceVerificationService {
|
|||||||
matchType: 'bl_number' as const,
|
matchType: 'bl_number' as const,
|
||||||
invoiceReference: matchResult.data[group.invoiceColumnName || 'RefFacture'] || `BL_${blNumber}`,
|
invoiceReference: matchResult.data[group.invoiceColumnName || 'RefFacture'] || `BL_${blNumber}`,
|
||||||
invoiceAmount: parseFloat(matchResult.data[group.nocodbAmountColumnName || 'amount'] || '0'),
|
invoiceAmount: parseFloat(matchResult.data[group.nocodbAmountColumnName || 'amount'] || '0'),
|
||||||
supplierName: foundSupplier || supplierName
|
supplierName: foundSupplier || supplierName,
|
||||||
|
dueDate: matchResult.data[group.nocodbDueDateColumnName || 'Echeance'] || null
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sauvegarder en cache si succès
|
// Sauvegarder en cache si succès
|
||||||
|
|||||||
+167
-77
@@ -7,6 +7,79 @@ import { db, pool } from "./db";
|
|||||||
|
|
||||||
console.log('🔍 Using development storage and authentication');
|
console.log('🔍 Using development storage and authentication');
|
||||||
|
|
||||||
|
// Fonction de normalisation des dates pour gérer différents formats de NocoDB
|
||||||
|
function normalizeDateString(dateString: string | null | undefined): string | null {
|
||||||
|
if (!dateString || typeof dateString !== 'string') return null;
|
||||||
|
|
||||||
|
const trimmed = dateString.trim();
|
||||||
|
if (!trimmed) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Si déjà au format ISO (YYYY-MM-DD), le retourner tel quel
|
||||||
|
if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format slash ou tiret : DD/MM/YYYY, MM/DD/YYYY, DD-MM-YYYY, MM-DD-YYYY
|
||||||
|
const slashMatch = trimmed.match(/^(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})$/);
|
||||||
|
if (slashMatch) {
|
||||||
|
const [, first, second, year] = slashMatch;
|
||||||
|
const firstNum = parseInt(first);
|
||||||
|
const secondNum = parseInt(second);
|
||||||
|
|
||||||
|
// Validation basique des valeurs
|
||||||
|
if (firstNum > 31 || secondNum > 31 || firstNum === 0 || secondNum === 0) {
|
||||||
|
console.warn(`⚠️ Date invalide (valeurs hors limites): ${trimmed}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Déterminer le format en fonction des valeurs
|
||||||
|
let day: string, month: string;
|
||||||
|
|
||||||
|
if (firstNum > 12) {
|
||||||
|
// first > 12 → forcément DD/MM (format français/européen)
|
||||||
|
day = first.padStart(2, '0');
|
||||||
|
month = second.padStart(2, '0');
|
||||||
|
} else if (secondNum > 12) {
|
||||||
|
// second > 12 → forcément MM/DD (format américain)
|
||||||
|
day = second.padStart(2, '0');
|
||||||
|
month = first.padStart(2, '0');
|
||||||
|
} else {
|
||||||
|
// Ambiguïté (les deux < 12) → on assume format français DD/MM par défaut
|
||||||
|
// Pour être plus sûr, on pourrait vérifier la configuration du groupe/locale
|
||||||
|
day = first.padStart(2, '0');
|
||||||
|
month = second.padStart(2, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation finale : mois entre 1-12, jour entre 1-31
|
||||||
|
const monthNum = parseInt(month);
|
||||||
|
const dayNum = parseInt(day);
|
||||||
|
if (monthNum < 1 || monthNum > 12 || dayNum < 1 || dayNum > 31) {
|
||||||
|
console.warn(`⚠️ Date invalide après parsing: ${trimmed} → month=${month}, day=${day}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Essayer de parser avec Date (format ISO complet avec heures)
|
||||||
|
const date = new Date(trimmed);
|
||||||
|
if (!isNaN(date.getTime())) {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si aucun format reconnu, retourner null (ne pas persister une date invalide)
|
||||||
|
console.warn(`⚠️ Format de date non reconnu: ${trimmed}`);
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Erreur normalisation date:', error);
|
||||||
|
return null; // Retourner null en cas d'erreur pour éviter de persister des données invalides
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Simple hash password function using crypto
|
// Simple hash password function using crypto
|
||||||
async function hashPasswordSimple(password: string) {
|
async function hashPasswordSimple(password: string) {
|
||||||
const crypto = await import('crypto');
|
const crypto = await import('crypto');
|
||||||
@@ -309,93 +382,76 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Récupérer le groupe avec la configuration NocoDB
|
// Récupérer le groupe
|
||||||
const group = await storage.getGroup(groupId);
|
const group = await storage.getGroup(groupId);
|
||||||
if (!group || !group.nocodbConfigId || !group.nocodbTableId) {
|
if (!group) {
|
||||||
return res.json({ schedules: [], message: 'Pas de configuration NocoDB pour ce groupe' });
|
return res.json({ schedules: [], message: 'Groupe non trouvé' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Récupérer la configuration NocoDB
|
console.log('📅 Récupération échéances depuis deliveries:', { groupId, groupName: group.name });
|
||||||
const nocodbConfig = await storage.getNocodbConfig(group.nocodbConfigId);
|
|
||||||
if (!nocodbConfig) {
|
// Récupérer toutes les livraisons du groupe
|
||||||
return res.json({ schedules: [], message: 'Configuration NocoDB non trouvée' });
|
const allDeliveries = await storage.getDeliveries();
|
||||||
|
const groupDeliveries = allDeliveries.filter((d: any) => d.groupId === groupId && d.invoiceReference);
|
||||||
|
|
||||||
|
// Séparer les livraisons avec et sans dueDate
|
||||||
|
const deliveriesWithDueDate = groupDeliveries.filter((d: any) => d.dueDate);
|
||||||
|
const deliveriesWithoutDueDate = groupDeliveries.filter((d: any) => !d.dueDate);
|
||||||
|
|
||||||
|
console.log(`📅 Livraisons avec échéance: ${deliveriesWithDueDate.length}, sans échéance: ${deliveriesWithoutDueDate.length}`);
|
||||||
|
|
||||||
|
// FALLBACK : Pour les livraisons sans dueDate, interroger NocoDB
|
||||||
|
const { InvoiceVerificationService } = await import('./invoiceVerification.js');
|
||||||
|
const verificationService = new InvoiceVerificationService();
|
||||||
|
|
||||||
|
for (const delivery of deliveriesWithoutDueDate) {
|
||||||
|
try {
|
||||||
|
const result = await verificationService.verifyInvoice(
|
||||||
|
delivery.invoiceReference!,
|
||||||
|
delivery.groupId,
|
||||||
|
false, // Ne pas forcer le refresh, utiliser cache si disponible
|
||||||
|
delivery.reconciled || false
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.exists && result.dueDate) {
|
||||||
|
// Normaliser la date avant de la stocker
|
||||||
|
const normalizedDate = normalizeDateString(result.dueDate);
|
||||||
|
if (normalizedDate) {
|
||||||
|
// Mettre à jour deliveries avec la date normalisée (caching)
|
||||||
|
await storage.updateDelivery(delivery.id, { dueDate: normalizedDate });
|
||||||
|
delivery.dueDate = normalizedDate;
|
||||||
|
console.log(`📅 Fallback: échéance récupérée et normalisée pour livraison #${delivery.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Fallback échéance échoué pour livraison #${delivery.id}:`, error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vérifier que les colonnes nécessaires sont configurées
|
// Combiner toutes les livraisons qui ont maintenant une dueDate
|
||||||
if (!group.nocodbDueDateColumnName || !group.invoiceColumnName || !group.nocodbAmountColumnName) {
|
const allDeliveriesWithDueDate = [...deliveriesWithDueDate, ...deliveriesWithoutDueDate.filter((d: any) => d.dueDate)];
|
||||||
return res.json({
|
|
||||||
schedules: [],
|
|
||||||
message: 'Configuration incomplète: colonnes date échéance, facture ou montant manquantes'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Récupérer tous les enregistrements de NocoDB
|
// Récupérer tous les fournisseurs pour le mapping du mode de paiement
|
||||||
const url = `${nocodbConfig.baseUrl}/api/v1/db/data/noco/${nocodbConfig.projectId}/${group.nocodbTableId}`;
|
|
||||||
|
|
||||||
console.log('📅 Récupération échéances NocoDB:', {
|
|
||||||
url,
|
|
||||||
groupId,
|
|
||||||
dueDateColumn: group.nocodbDueDateColumnName
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"xc-token": nocodbConfig.apiToken,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error('❌ Erreur NocoDB:', response.status, response.statusText);
|
|
||||||
return res.status(500).json({ error: 'Erreur lors de la récupération des données NocoDB' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
// Récupérer tous les fournisseurs pour le mapping
|
|
||||||
const allSuppliers = await storage.getSuppliers();
|
const allSuppliers = await storage.getSuppliers();
|
||||||
|
const supplierMap = new Map(allSuppliers.map((s: any) => [s.id, s]));
|
||||||
|
|
||||||
// Fonction de normalisation pour comparer les noms de fournisseurs
|
// Formatter les échéances
|
||||||
const normalizeName = (name: string) => {
|
const schedules = allDeliveriesWithDueDate.map((delivery: any) => {
|
||||||
return name
|
const supplier = supplierMap.get(delivery.supplierId);
|
||||||
.toLowerCase()
|
|
||||||
.trim()
|
|
||||||
.normalize("NFD") // Décompose les accents
|
|
||||||
.replace(/[\u0300-\u036f]/g, "") // Supprime les diacritiques
|
|
||||||
.replace(/\s+/g, ' '); // Normalise les espaces multiples
|
|
||||||
};
|
|
||||||
|
|
||||||
// Créer une map avec noms normalisés
|
return {
|
||||||
const supplierMap = new Map(allSuppliers.map(s => [normalizeName(s.name), s]));
|
id: delivery.id,
|
||||||
|
invoiceReference: delivery.invoiceReference,
|
||||||
// Traiter les données et filtrer les enregistrements avec une date d'échéance
|
dueDate: delivery.dueDate,
|
||||||
const schedules = (data.list || [])
|
amount: delivery.invoiceAmount ? parseFloat(delivery.invoiceAmount) : 0,
|
||||||
.map((record: any) => {
|
supplierName: supplier?.name || 'Fournisseur inconnu',
|
||||||
const dueDate = record[group.nocodbDueDateColumnName!];
|
paymentMethod: supplier?.paymentMethod || null,
|
||||||
const invoiceRef = record[group.invoiceColumnName!];
|
groupId: group.id,
|
||||||
const amount = record[group.nocodbAmountColumnName!];
|
groupName: group.name
|
||||||
const supplierName = record[group.nocodbSupplierColumnName || 'Fournisseur'];
|
};
|
||||||
|
});
|
||||||
if (!dueDate) return null;
|
|
||||||
|
|
||||||
// Chercher le fournisseur correspondant avec normalisation
|
|
||||||
const normalizedSupplierName = normalizeName(supplierName || '');
|
|
||||||
const supplier = supplierMap.get(normalizedSupplierName);
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: record.Id || record.id,
|
|
||||||
invoiceReference: invoiceRef,
|
|
||||||
dueDate: dueDate,
|
|
||||||
amount: amount ? parseFloat(amount) : 0,
|
|
||||||
supplierName: supplierName,
|
|
||||||
paymentMethod: supplier?.paymentMethod || null,
|
|
||||||
groupId: group.id,
|
|
||||||
groupName: group.name
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter((s: any) => s !== null);
|
|
||||||
|
|
||||||
|
console.log(`📅 Total échéances retournées: ${schedules.length}`);
|
||||||
res.json({ schedules });
|
res.json({ schedules });
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -1374,6 +1430,40 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GESTION ÉCHÉANCE : Si la référence facture change, mettre à jour la date d'échéance
|
||||||
|
if (data.invoiceReference !== undefined && data.invoiceReference !== delivery.invoiceReference) {
|
||||||
|
if (data.invoiceReference && data.invoiceReference.trim()) {
|
||||||
|
// Nouvelle référence facture : reverifier dans NocoDB pour récupérer l'échéance
|
||||||
|
try {
|
||||||
|
const { InvoiceVerificationService } = await import('./invoiceVerification.js');
|
||||||
|
const verificationService = new InvoiceVerificationService();
|
||||||
|
const result = await verificationService.verifyInvoice(
|
||||||
|
data.invoiceReference,
|
||||||
|
delivery.groupId,
|
||||||
|
true, // forceRefresh
|
||||||
|
delivery.reconciled || false
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.exists && result.dueDate) {
|
||||||
|
// Normaliser la date avant de la stocker
|
||||||
|
data.dueDate = normalizeDateString(result.dueDate);
|
||||||
|
console.log(`📅 Date d'échéance récupérée et normalisée: ${data.dueDate} (original: ${result.dueDate})`);
|
||||||
|
} else {
|
||||||
|
data.dueDate = null;
|
||||||
|
console.log(`📅 Aucune date d'échéance trouvée dans NocoDB`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Erreur récupération échéance:', error);
|
||||||
|
// Ne pas bloquer la mise à jour, juste ne pas avoir d'échéance
|
||||||
|
data.dueDate = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Référence facture vidée : vider aussi l'échéance
|
||||||
|
data.dueDate = null;
|
||||||
|
console.log(`📅 Référence facture vidée, échéance également vidée`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const updatedDelivery = await storage.updateDelivery(id, data);
|
const updatedDelivery = await storage.updateDelivery(id, data);
|
||||||
console.log('✅ Delivery updated successfully:', { id, updatedDelivery });
|
console.log('✅ Delivery updated successfully:', { id, updatedDelivery });
|
||||||
|
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ export const deliveries = pgTable("deliveries", {
|
|||||||
blAmount: decimal("bl_amount", { precision: 10, scale: 2 }), // Montant BL
|
blAmount: decimal("bl_amount", { precision: 10, scale: 2 }), // Montant BL
|
||||||
invoiceReference: varchar("invoice_reference"), // Référence facture
|
invoiceReference: varchar("invoice_reference"), // Référence facture
|
||||||
invoiceAmount: decimal("invoice_amount", { precision: 10, scale: 2 }), // Montant facture
|
invoiceAmount: decimal("invoice_amount", { precision: 10, scale: 2 }), // Montant facture
|
||||||
|
dueDate: date("due_date"), // Date d'échéance de paiement (récupérée depuis NocoDB)
|
||||||
reconciled: boolean("reconciled").default(false), // Rapprochement effectué
|
reconciled: boolean("reconciled").default(false), // Rapprochement effectué
|
||||||
validatedAt: timestamp("validated_at"), // Date de validation de la livraison
|
validatedAt: timestamp("validated_at"), // Date de validation de la livraison
|
||||||
// Champs pour le contrôle des livraisons (fournisseurs nécessitant un contrôle)
|
// Champs pour le contrôle des livraisons (fournisseurs nécessitant un contrôle)
|
||||||
|
|||||||
Reference in new issue
Block a user