mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-24 07:28:19 +02:00
Merge pull request #78 from R0m1k3/1.1
Add NocoDB invoice verification to reconcile deliveries automatically
This commit is contained in:
4 files changed
+474
-5
No files matched your search
@@ -12,7 +12,7 @@ import { useStore } from "@/components/Layout";
|
|||||||
import { useAuthUnified } from "@/hooks/useAuthUnified";
|
import { useAuthUnified } from "@/hooks/useAuthUnified";
|
||||||
import { usePermissions } from "@shared/permissions";
|
import { usePermissions } from "@shared/permissions";
|
||||||
import { Pagination, usePagination } from "@/components/ui/pagination";
|
import { Pagination, usePagination } from "@/components/ui/pagination";
|
||||||
import { Search, Edit, FileText, Settings, Eye, AlertTriangle, X, Check, Trash2, Ban, Filter, Upload } from "lucide-react";
|
import { Search, Edit, FileText, Settings, Eye, AlertTriangle, X, Check, Trash2, Ban, Filter, Upload, CheckCircle, XCircle, Clock } from "lucide-react";
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
@@ -57,12 +57,99 @@ export default function BLReconciliation() {
|
|||||||
const [invoiceType, setInvoiceType] = useState<string>("");
|
const [invoiceType, setInvoiceType] = useState<string>("");
|
||||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
|
|
||||||
|
// État pour le système de vérification de facture
|
||||||
|
const [verificationResults, setVerificationResults] = useState<Record<number, any>>({});
|
||||||
|
const [verifyingDeliveries, setVerifyingDeliveries] = useState<Set<number>>(new Set());
|
||||||
|
|
||||||
// Récupérer les fournisseurs pour la logique automatique
|
// Récupérer les fournisseurs pour la logique automatique
|
||||||
const { data: suppliers = [] } = useQuery<any[]>({
|
const { data: suppliers = [] } = useQuery<any[]>({
|
||||||
queryKey: ['/api/suppliers'],
|
queryKey: ['/api/suppliers'],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Fonction de vérification de facture
|
||||||
|
const verifyInvoiceMutation = useMutation({
|
||||||
|
mutationFn: async ({ deliveryId, invoiceReference, blNumber }: { deliveryId: number; invoiceReference?: string; blNumber?: string }) => {
|
||||||
|
const response = await apiRequest(`/api/deliveries/${deliveryId}/verify-invoice`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ invoiceReference, blNumber }),
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json();
|
||||||
|
throw new Error(error.message || 'Erreur de vérification');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
onSuccess: (result, variables) => {
|
||||||
|
setVerificationResults(prev => ({
|
||||||
|
...prev,
|
||||||
|
[variables.deliveryId]: result
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Auto-remplissage si facture trouvée via BL
|
||||||
|
if (result.exists && result.matchType === 'bl_number' && result.invoiceReference) {
|
||||||
|
// Auto-remplir les champs dans la livraison
|
||||||
|
updateDeliveryMutation.mutate({
|
||||||
|
id: variables.deliveryId,
|
||||||
|
invoiceReference: result.invoiceReference,
|
||||||
|
invoiceAmount: result.invoiceAmount
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setVerifyingDeliveries(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(variables.deliveryId);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error, variables) => {
|
||||||
|
console.error('Erreur vérification facture:', error);
|
||||||
|
setVerificationResults(prev => ({
|
||||||
|
...prev,
|
||||||
|
[variables.deliveryId]: {
|
||||||
|
exists: false,
|
||||||
|
matchType: 'none',
|
||||||
|
errorMessage: error instanceof Error ? error.message : 'Erreur inconnue'
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
setVerifyingDeliveries(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(variables.deliveryId);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Erreur de vérification",
|
||||||
|
description: error instanceof Error ? error.message : 'Erreur inconnue',
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fonction pour déclencher la vérification
|
||||||
|
const handleVerifyInvoice = (delivery: any) => {
|
||||||
|
if (!delivery.group?.webhookUrl) {
|
||||||
|
toast({
|
||||||
|
title: "Vérification non disponible",
|
||||||
|
description: "Ce magasin n'a pas de configuration NocoDB",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setVerifyingDeliveries(prev => new Set(prev).add(delivery.id));
|
||||||
|
|
||||||
|
verifyInvoiceMutation.mutate({
|
||||||
|
deliveryId: delivery.id,
|
||||||
|
invoiceReference: delivery.invoiceReference,
|
||||||
|
blNumber: delivery.blNumber
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Récupérer les livraisons validées avec BL
|
// Récupérer les livraisons validées avec BL
|
||||||
const { data: deliveriesWithBL = [], isLoading } = useQuery({
|
const { data: deliveriesWithBL = [], isLoading } = useQuery({
|
||||||
queryKey: ['/api/deliveries/bl', selectedStoreId],
|
queryKey: ['/api/deliveries/bl', selectedStoreId],
|
||||||
@@ -536,9 +623,40 @@ export default function BLReconciliation() {
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<div className={`text-sm ${delivery.reconciled !== true ? 'font-medium text-gray-900' : 'text-gray-600'}`}>
|
<div className="flex items-center space-x-2">
|
||||||
{delivery.invoiceReference || (
|
<div className={`text-sm ${delivery.reconciled !== true ? 'font-medium text-gray-900' : 'text-gray-600'}`}>
|
||||||
<span className="text-gray-400 italic">Non renseigné</span>
|
{delivery.invoiceReference || (
|
||||||
|
<span className="text-gray-400 italic">Non renseigné</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Icône de vérification de facture */}
|
||||||
|
{delivery.group?.webhookUrl && (
|
||||||
|
<div className="flex items-center">
|
||||||
|
{verifyingDeliveries.has(delivery.id) ? (
|
||||||
|
<Clock className="h-4 w-4 text-blue-500 animate-spin" title="Vérification en cours..." />
|
||||||
|
) : verificationResults[delivery.id] ? (
|
||||||
|
verificationResults[delivery.id].exists ? (
|
||||||
|
<CheckCircle
|
||||||
|
className="h-4 w-4 text-green-500 cursor-help"
|
||||||
|
title={`Facture vérifiée (${verificationResults[delivery.id].matchType === 'invoice_ref' ? 'référence directe' : 'via BL'})`}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<XCircle
|
||||||
|
className="h-4 w-4 text-red-500 cursor-help"
|
||||||
|
title={`Facture non trouvée: ${verificationResults[delivery.id].errorMessage || 'Aucune correspondance'}`}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() => handleVerifyInvoice(delivery)}
|
||||||
|
className="h-4 w-4 text-gray-400 hover:text-blue-500 transition-colors"
|
||||||
|
title="Cliquer pour vérifier la facture"
|
||||||
|
>
|
||||||
|
<Search className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -0,0 +1,277 @@
|
|||||||
|
import { db } from './db.js';
|
||||||
|
import { nocodbConfig, invoiceVerificationCache, groups } from '@shared/schema';
|
||||||
|
import { eq, and, desc } from 'drizzle-orm';
|
||||||
|
|
||||||
|
interface InvoiceVerificationResult {
|
||||||
|
exists: boolean;
|
||||||
|
matchType: 'invoice_ref' | 'bl_number' | 'none';
|
||||||
|
invoiceReference?: string;
|
||||||
|
invoiceAmount?: string;
|
||||||
|
supplierMatch: boolean;
|
||||||
|
errorMessage?: string;
|
||||||
|
apiCallTime?: number;
|
||||||
|
cacheHit: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NocodbInvoice {
|
||||||
|
Id: number;
|
||||||
|
[key: string]: any; // Pour les colonnes dynamiques
|
||||||
|
}
|
||||||
|
|
||||||
|
class InvoiceVerificationService {
|
||||||
|
private async getNocodbConfig(): Promise<any> {
|
||||||
|
const [config] = await db
|
||||||
|
.select()
|
||||||
|
.from(nocodbConfig)
|
||||||
|
.where(eq(nocodbConfig.isActive, true))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
throw new Error('Aucune configuration NocoDB active trouvée');
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateCacheKey(groupId: number, invoiceRef: string, supplierName: string): string {
|
||||||
|
return `${groupId}:${invoiceRef}:${supplierName}`.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getCachedResult(cacheKey: string): Promise<InvoiceVerificationResult | null> {
|
||||||
|
try {
|
||||||
|
const [cached] = await db
|
||||||
|
.select()
|
||||||
|
.from(invoiceVerificationCache)
|
||||||
|
.where(and(
|
||||||
|
eq(invoiceVerificationCache.cacheKey, cacheKey),
|
||||||
|
// Cache valide pour 24 heures
|
||||||
|
eq(invoiceVerificationCache.expiresAt, new Date(Date.now() + 24 * 60 * 60 * 1000))
|
||||||
|
))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (cached && new Date() < new Date(cached.expiresAt)) {
|
||||||
|
console.log('🎯 Cache hit pour:', cacheKey);
|
||||||
|
return {
|
||||||
|
exists: cached.exists,
|
||||||
|
matchType: cached.matchType as any,
|
||||||
|
invoiceReference: cached.invoiceReference,
|
||||||
|
supplierMatch: !!cached.supplierName,
|
||||||
|
cacheHit: true,
|
||||||
|
apiCallTime: cached.apiCallTime || 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Erreur lors de la lecture du cache:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setCachedResult(
|
||||||
|
cacheKey: string,
|
||||||
|
groupId: number,
|
||||||
|
result: InvoiceVerificationResult,
|
||||||
|
invoiceRef: string,
|
||||||
|
supplierName: string
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
await db.insert(invoiceVerificationCache).values({
|
||||||
|
cacheKey,
|
||||||
|
groupId,
|
||||||
|
invoiceReference: invoiceRef,
|
||||||
|
supplierName,
|
||||||
|
exists: result.exists,
|
||||||
|
matchType: result.matchType,
|
||||||
|
errorMessage: result.errorMessage,
|
||||||
|
cacheHit: false,
|
||||||
|
apiCallTime: result.apiCallTime || 0,
|
||||||
|
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000) // 24h
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('💾 Résultat mis en cache:', cacheKey);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Erreur lors de la mise en cache:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async searchInNocodb(
|
||||||
|
config: any,
|
||||||
|
group: any,
|
||||||
|
searchValue: string,
|
||||||
|
searchType: 'invoice' | 'bl'
|
||||||
|
): Promise<{ found: boolean; invoice?: any }> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Définir la colonne de recherche selon le groupe
|
||||||
|
const searchColumn = searchType === 'invoice'
|
||||||
|
? group.invoiceColumnName
|
||||||
|
: group.nocodbBlColumnName;
|
||||||
|
|
||||||
|
if (!searchColumn) {
|
||||||
|
throw new Error(`Colonne ${searchType} non configurée pour ce magasin`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${config.baseUrl}/api/v2/tables/${group.nocodbTableName}/records`;
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
where: `(${searchColumn},eq,${searchValue})`
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetch(`${url}?${params}`, {
|
||||||
|
headers: {
|
||||||
|
'xc-token': config.apiToken,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`NocoDB API error: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const invoices = data.list || [];
|
||||||
|
|
||||||
|
console.log(`🔍 Recherche NocoDB ${searchType}:`, {
|
||||||
|
searchValue,
|
||||||
|
found: invoices.length > 0,
|
||||||
|
table: group.nocodbTableName,
|
||||||
|
column: searchColumn,
|
||||||
|
apiTime: Date.now() - startTime
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
found: invoices.length > 0,
|
||||||
|
invoice: invoices[0] || null
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Erreur recherche NocoDB ${searchType}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyInvoiceReference(
|
||||||
|
invoiceRef: string,
|
||||||
|
blNumber: string,
|
||||||
|
supplierName: string,
|
||||||
|
groupId: number
|
||||||
|
): Promise<InvoiceVerificationResult> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Vérifier le cache d'abord
|
||||||
|
const cacheKey = this.generateCacheKey(groupId, invoiceRef, supplierName);
|
||||||
|
const cachedResult = await this.getCachedResult(cacheKey);
|
||||||
|
if (cachedResult) {
|
||||||
|
return cachedResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Récupérer la configuration NocoDB
|
||||||
|
const config = await this.getNocodbConfig();
|
||||||
|
|
||||||
|
// 3. Récupérer les informations du groupe
|
||||||
|
const [group] = await db
|
||||||
|
.select()
|
||||||
|
.from(groups)
|
||||||
|
.where(eq(groups.id, groupId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!group || !group.nocodbTableName) {
|
||||||
|
throw new Error('Configuration NocoDB manquante pour ce magasin');
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: InvoiceVerificationResult = {
|
||||||
|
exists: false,
|
||||||
|
matchType: 'none',
|
||||||
|
supplierMatch: false,
|
||||||
|
cacheHit: false,
|
||||||
|
apiCallTime: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
// 4. Recherche primaire : par référence facture
|
||||||
|
if (invoiceRef && invoiceRef.trim()) {
|
||||||
|
const searchResult = await this.searchInNocodb(config, group, invoiceRef, 'invoice');
|
||||||
|
|
||||||
|
if (searchResult.found) {
|
||||||
|
// Vérifier la correspondance du fournisseur
|
||||||
|
const supplierColumn = group.nocodbSupplierColumnName;
|
||||||
|
const invoiceSupplier = supplierColumn ? searchResult.invoice[supplierColumn] : '';
|
||||||
|
const supplierMatch = !supplierColumn ||
|
||||||
|
invoiceSupplier.toLowerCase().includes(supplierName.toLowerCase()) ||
|
||||||
|
supplierName.toLowerCase().includes(invoiceSupplier.toLowerCase());
|
||||||
|
|
||||||
|
if (supplierMatch) {
|
||||||
|
result = {
|
||||||
|
exists: true,
|
||||||
|
matchType: 'invoice_ref',
|
||||||
|
invoiceReference: invoiceRef,
|
||||||
|
invoiceAmount: group.nocodbAmountColumnName ?
|
||||||
|
searchResult.invoice[group.nocodbAmountColumnName] : undefined,
|
||||||
|
supplierMatch: true,
|
||||||
|
cacheHit: false,
|
||||||
|
apiCallTime: Date.now() - startTime
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
result.errorMessage = 'Référence trouvée mais fournisseur ne correspond pas';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Recherche secondaire : par numéro de BL si pas de résultat avec la facture
|
||||||
|
if (!result.exists && blNumber && blNumber.trim()) {
|
||||||
|
const searchResult = await this.searchInNocodb(config, group, blNumber, 'bl');
|
||||||
|
|
||||||
|
if (searchResult.found) {
|
||||||
|
// Vérifier la correspondance du fournisseur
|
||||||
|
const supplierColumn = group.nocodbSupplierColumnName;
|
||||||
|
const invoiceSupplier = supplierColumn ? searchResult.invoice[supplierColumn] : '';
|
||||||
|
const supplierMatch = !supplierColumn ||
|
||||||
|
invoiceSupplier.toLowerCase().includes(supplierName.toLowerCase()) ||
|
||||||
|
supplierName.toLowerCase().includes(invoiceSupplier.toLowerCase());
|
||||||
|
|
||||||
|
if (supplierMatch) {
|
||||||
|
const foundInvoiceRef = group.invoiceColumnName ?
|
||||||
|
searchResult.invoice[group.invoiceColumnName] : '';
|
||||||
|
const foundAmount = group.nocodbAmountColumnName ?
|
||||||
|
searchResult.invoice[group.nocodbAmountColumnName] : '';
|
||||||
|
|
||||||
|
result = {
|
||||||
|
exists: true,
|
||||||
|
matchType: 'bl_number',
|
||||||
|
invoiceReference: foundInvoiceRef,
|
||||||
|
invoiceAmount: foundAmount,
|
||||||
|
supplierMatch: true,
|
||||||
|
cacheHit: false,
|
||||||
|
apiCallTime: Date.now() - startTime
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
result.errorMessage = 'BL trouvé mais fournisseur ne correspond pas';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si rien trouvé
|
||||||
|
if (!result.exists) {
|
||||||
|
result.apiCallTime = Date.now() - startTime;
|
||||||
|
result.errorMessage = result.errorMessage || 'Aucune correspondance trouvée';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Mettre en cache le résultat
|
||||||
|
await this.setCachedResult(cacheKey, groupId, result, invoiceRef, supplierName);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Erreur vérification facture:', error);
|
||||||
|
return {
|
||||||
|
exists: false,
|
||||||
|
matchType: 'none',
|
||||||
|
supplierMatch: false,
|
||||||
|
cacheHit: false,
|
||||||
|
errorMessage: error instanceof Error ? error.message : 'Erreur inconnue',
|
||||||
|
apiCallTime: Date.now() - startTime
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const invoiceVerificationService = new InvoiceVerificationService();
|
||||||
@@ -37,6 +37,7 @@ import {
|
|||||||
} from "@shared/schema";
|
} from "@shared/schema";
|
||||||
import { hasPermission } from "@shared/permissions";
|
import { hasPermission } from "@shared/permissions";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { invoiceVerificationService } from "./invoiceVerification.js";
|
||||||
|
|
||||||
export async function registerRoutes(app: Express): Promise<Server> {
|
export async function registerRoutes(app: Express): Promise<Server> {
|
||||||
// Health check endpoint for Docker
|
// Health check endpoint for Docker
|
||||||
@@ -831,6 +832,56 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Route de vérification de facture NocoDB
|
||||||
|
app.post('/api/deliveries/:id/verify-invoice', isAuthenticated, async (req: any, res) => {
|
||||||
|
try {
|
||||||
|
const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id);
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({ message: "User not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const deliveryId = parseInt(req.params.id);
|
||||||
|
const delivery = await storage.getDelivery(deliveryId);
|
||||||
|
|
||||||
|
if (!delivery) {
|
||||||
|
return res.status(404).json({ message: "Delivery not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check permissions
|
||||||
|
if (!hasPermission(user.role, 'deliveries', 'view')) {
|
||||||
|
return res.status(403).json({ message: "Insufficient permissions" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.role !== 'admin') {
|
||||||
|
const userGroupIds = user.userGroups?.map((ug: any) => ug.groupId) || [];
|
||||||
|
if (!userGroupIds.includes(delivery.groupId)) {
|
||||||
|
return res.status(403).json({ message: "Access denied to this group" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { invoiceReference, blNumber } = req.body;
|
||||||
|
|
||||||
|
if (!delivery.supplier || !delivery.group) {
|
||||||
|
return res.status(400).json({ message: "Delivery missing supplier or group information" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await invoiceVerificationService.verifyInvoiceReference(
|
||||||
|
invoiceReference || delivery.invoiceReference || '',
|
||||||
|
blNumber || delivery.blNumber || '',
|
||||||
|
delivery.supplier.name,
|
||||||
|
delivery.groupId
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error verifying invoice:", error);
|
||||||
|
res.status(500).json({
|
||||||
|
message: "Failed to verify invoice",
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.post('/api/deliveries/:id/validate', isAuthenticated, async (req: any, res) => {
|
app.post('/api/deliveries/:id/validate', isAuthenticated, async (req: any, res) => {
|
||||||
try {
|
try {
|
||||||
const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id);
|
const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id);
|
||||||
|
|||||||
+24
-1
@@ -152,7 +152,24 @@ export const nocodbConfig = pgTable("nocodb_config", {
|
|||||||
apiToken: varchar("api_token").notNull(), // Personal API Token
|
apiToken: varchar("api_token").notNull(), // Personal API Token
|
||||||
description: text("description"), // Description de la configuration
|
description: text("description"), // Description de la configuration
|
||||||
isActive: boolean("is_active").default(true), // Configuration active ou non
|
isActive: boolean("is_active").default(true), // Configuration active ou non
|
||||||
createdBy: varchar("created_by").notNull(),
|
createdBy: varchar("created_by"),
|
||||||
|
createdAt: timestamp("created_at").defaultNow(),
|
||||||
|
updatedAt: timestamp("updated_at").defaultNow(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cache de vérification des factures pour éviter les re-vérifications
|
||||||
|
export const invoiceVerificationCache = pgTable("invoice_verification_cache", {
|
||||||
|
id: serial("id").primaryKey(),
|
||||||
|
cacheKey: varchar("cache_key", { length: 255 }).notNull(), // Clé unique de cache
|
||||||
|
groupId: integer("group_id").notNull(), // ID du magasin
|
||||||
|
invoiceReference: varchar("invoice_reference", { length: 255 }).notNull(), // Référence facture
|
||||||
|
supplierName: varchar("supplier_name", { length: 255 }), // Nom du fournisseur
|
||||||
|
exists: boolean("exists").notNull(), // Facture trouvée ou non
|
||||||
|
matchType: varchar("match_type", { length: 50 }).notNull(), // Type de correspondance (invoice_ref, bl_number)
|
||||||
|
errorMessage: text("error_message"), // Message d'erreur si applicable
|
||||||
|
cacheHit: boolean("cache_hit").default(false), // Cache hit ou non
|
||||||
|
apiCallTime: integer("api_call_time"), // Temps d'appel API en ms
|
||||||
|
expiresAt: timestamp("expires_at").notNull(), // Date d'expiration du cache
|
||||||
createdAt: timestamp("created_at").defaultNow(),
|
createdAt: timestamp("created_at").defaultNow(),
|
||||||
updatedAt: timestamp("updated_at").defaultNow(),
|
updatedAt: timestamp("updated_at").defaultNow(),
|
||||||
});
|
});
|
||||||
@@ -444,6 +461,12 @@ export const insertNocodbConfigSchema = createInsertSchema(nocodbConfig).omit({
|
|||||||
updatedAt: true,
|
updatedAt: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const insertInvoiceVerificationCacheSchema = createInsertSchema(invoiceVerificationCache).omit({
|
||||||
|
id: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
});
|
||||||
|
|
||||||
export const insertCustomerOrderSchema = createInsertSchema(customerOrders).omit({
|
export const insertCustomerOrderSchema = createInsertSchema(customerOrders).omit({
|
||||||
id: true,
|
id: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
|
|||||||
Reference in new issue
Block a user