feat(v2.0.3): Marchés hybrides et améliorations multiples
Fonctionnalités principales : 1. Marchés hybrides - Onglet Mercurial - Ajout onglet Mercurial avec style distinct (vert, gras, blanc) - Affichage des produits mercuriaux pour marchés hybrides - Filtrage automatique des produits "Hors Marché 999" - Documentation Phase 2 avec CAS 1 et CAS 2 de marchés hybrides - Règles métier pour validation différenciée (devis 100% mercurial vs mixte) 2. Corrections bugs - Fix flag chkChange sur onglet "Sélection Produits" (callback asynchrone) - Plus d'alerte intempestive après sauvegarde des produits 3. Outils de déploiement - Nouveau script deploy-file.sh pour déploiement unitaire (DEV/PROD) - Amélioration deploy-cleo.sh 4. Gestion multi-contacts (v2.0.3) - Contrôleur AJAX cjxcontacts.php - Script migration clients_contacts - Documentation complète 5. Documentation - Mise à jour TODO.md avec Phase 2 marchés hybrides - Mise à jour README.md v2.0.3 - Ajout RULES.md - Ajout migration_clients_contacts.sql 6. Nettoyage - Suppression fichiers obsolètes (conf_new.php, conf_old.php, uof_linet_20250911.sql) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
|
||||
CLEO est une application web de gestion de devis développée en PHP 8.3 pour les PME. Elle utilise une architecture MVC classique avec un framework maison appelé "d6".
|
||||
|
||||
**Version actuelle** : 2.0.1 (migration complétée le 12 septembre 2025)
|
||||
**Version actuelle** : 2.0.3 (gestion multi-contacts complétée le 21 octobre 2025)
|
||||
|
||||
## Architecture technique
|
||||
|
||||
@@ -53,7 +53,9 @@ cleo/
|
||||
- Gestion des remises par paliers de quantité
|
||||
|
||||
2. **Gestion des clients** (`cclients.php`)
|
||||
- Base clients avec contacts
|
||||
- Base clients avec contacts multiples (table `clients_contacts`)
|
||||
- Gestion des contacts via modale Bootstrap intégrée aux devis
|
||||
- Contact principal automatique avec indicateur visuel
|
||||
- Segmentation par secteur géographique
|
||||
- Types de clients paramétrables
|
||||
- Import/export de données
|
||||
@@ -82,10 +84,11 @@ cleo/
|
||||
- **Sécurité** : Requêtes préparées systématiques
|
||||
|
||||
### Tables principales
|
||||
- `devis` : Table principale des devis
|
||||
- `devis` : Table principale des devis (champ `fk_contact` depuis v2.0.3)
|
||||
- `devis_produits` : Lignes de produits des devis
|
||||
- `devis_histo` : Historique des modifications
|
||||
- `clients` : Base clients
|
||||
- `clients_contacts` : Contacts multiples par client (v2.0.3)
|
||||
- `produits` : Catalogue produits
|
||||
- `produits_familles` : Familles de produits avec marges
|
||||
- `marches` : Référentiel des marchés
|
||||
@@ -95,22 +98,32 @@ cleo/
|
||||
|
||||
## Points de sécurité
|
||||
|
||||
### Vulnérabilités corrigées (v2.0.1)
|
||||
### Vulnérabilités corrigées
|
||||
|
||||
✅ **1. Stockage des mots de passe**
|
||||
✅ **v2.0.1 - Stockage des mots de passe**
|
||||
- Credentials externalisés dans `.env`
|
||||
- Variables d'environnement utilisées systématiquement
|
||||
|
||||
✅ **2. Protection contre les injections SQL**
|
||||
✅ **v2.0.1 - Protection contre les injections SQL**
|
||||
- Migration complète vers PDO
|
||||
- Requêtes préparées dans la classe `Database`
|
||||
- Pattern Singleton pour la connexion
|
||||
|
||||
✅ **3. Gestion des erreurs sécurisée**
|
||||
✅ **v2.0.1 - Gestion des erreurs sécurisée**
|
||||
- Classe `Database` avec gestion d'erreurs centralisée
|
||||
- Logging contrôlé par variables d'environnement
|
||||
- Mode debug désactivable en production
|
||||
|
||||
✅ **v2.0.2 - Corrections critiques**
|
||||
- Sanitisation stricte des entrées utilisateur
|
||||
- Validation des paramètres AJAX
|
||||
- Fonction `nettoie_input()` utilisée systématiquement
|
||||
|
||||
✅ **v2.0.3 - Gestion multi-contacts sécurisée**
|
||||
- Contrôleur AJAX `cjxcontacts.php` avec requêtes préparées
|
||||
- Validation des foreign keys et soft delete
|
||||
- Prévention de suppression du dernier contact actif
|
||||
|
||||
### Vulnérabilités restantes à traiter
|
||||
|
||||
#### 1. Injections SQL résiduelles
|
||||
@@ -208,15 +221,28 @@ cleo/
|
||||
|
||||
## Conclusion
|
||||
|
||||
CLEO v2.0.1 représente une évolution majeure avec la migration réussie vers une architecture sécurisée :
|
||||
- ✅ Base de données unique et centralisée
|
||||
- ✅ Connexions PDO avec requêtes préparées
|
||||
- ✅ Configuration externalisée
|
||||
- ✅ Séparation application/base de données
|
||||
CLEO v2.0.3 représente l'aboutissement de trois itérations majeures d'amélioration :
|
||||
|
||||
Les priorités de sécurité critiques ont été adressées. L'application peut maintenant évoluer sereinement vers des standards plus modernes tout en maintenant sa stabilité opérationnelle.
|
||||
**v2.0.1 - Architecture sécurisée**
|
||||
- Base de données unique et centralisée
|
||||
- Connexions PDO avec requêtes préparées
|
||||
- Configuration externalisée
|
||||
- Séparation application/base de données
|
||||
|
||||
**v2.0.2 - Sécurité renforcée**
|
||||
- Sanitisation systématique des entrées
|
||||
- Validation stricte des paramètres AJAX
|
||||
- Corrections de vulnérabilités critiques
|
||||
|
||||
**v2.0.3 - Gestion multi-contacts**
|
||||
- Migration vers table relationnelle `clients_contacts`
|
||||
- Interface modale intégrée dans les devis
|
||||
- CRUD complet avec soft delete
|
||||
- Gestion automatique du contact principal
|
||||
|
||||
L'application dispose maintenant d'une base solide pour évoluer vers des standards modernes tout en maintenant sa stabilité opérationnelle.
|
||||
|
||||
---
|
||||
|
||||
*Document mis à jour le 12 septembre 2025*
|
||||
*Version 2.0.1 - Post-migration*
|
||||
*Document mis à jour le 21 octobre 2025*
|
||||
*Version 2.0.3 - Gestion multi-contacts*
|
||||
111
docs/RULES.md
Normal file
111
docs/RULES.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# Règles métier - Application CLEO
|
||||
|
||||
Ce document recense les règles métier et de développement identifiées dans l'application CLEO.
|
||||
|
||||
## 1. Gestion des rôles et permissions
|
||||
|
||||
### 1.1 Hiérarchie des rôles
|
||||
- **DIR-CO** (fk_role = 1) : Direction commerciale
|
||||
- Accès complet aux devis (propres + statut >= 2)
|
||||
- Vision globale de l'activité
|
||||
|
||||
- **DV** (fk_role = 2) : Directeur des ventes
|
||||
- Accès à ses propres devis
|
||||
- Accès aux devis de ses RR subordonnés (statut >= 3)
|
||||
- Récupération des RR via `fk_parent` dans la table `users`
|
||||
|
||||
- **RR** (fk_role = 3) : Responsable régional
|
||||
- Accès uniquement à ses propres devis
|
||||
|
||||
- **Admin** (fk_role = 90) : Administration système
|
||||
- Accès complet à l'administration
|
||||
|
||||
### 1.2 Visibilité des devis (mdevis.php)
|
||||
La clause WHERE pour filtrer les devis dépend du rôle :
|
||||
- **DIR-CO** : `d.fk_user = :fkUser OR d.fk_statut_devis >= 2`
|
||||
- **DV** : `d.fk_user = :fkUser OR (d.fk_statut_devis >= 3 AND d.fk_user IN ([RR_IDS]))`
|
||||
- **RR** : `d.fk_user = :fkUser`
|
||||
|
||||
## 2. Gestion des marchés et produits
|
||||
|
||||
### 2.1 Types de marchés
|
||||
- **Marché standard** : Contient ses propres produits uniquement
|
||||
- **Marché hybride** (`chk_marche_hybride = 1`) : Combine les produits du marché + produits du marché 999
|
||||
- **Marché avec remise sur TG** (`chk_remise_sur_tg = 1`) : Charge uniquement les produits du marché 999
|
||||
- **Marché 999** : Marché "Hors marché" ou "Tarif général"
|
||||
|
||||
### 2.2 Chargement des produits (load_devis_marche_produits)
|
||||
|
||||
| Type de marché | chk_remise_sur_tg | chk_marche_hybride | Produits chargés |
|
||||
|---------------|-------------------|-------------------|------------------|
|
||||
| Spécifique | 1 | - | Tous les produits du marché 999 |
|
||||
| Spécifique | 0 | 0 | Produits du marché uniquement |
|
||||
| Spécifique | 0 | 1 | Produits du marché + produits du 999 non présents |
|
||||
| 999 (Hors marché) | - | - | Tous les produits du marché 999 |
|
||||
|
||||
### 2.3 Terme "Purchasing"
|
||||
Quand `terme_achat = 'Purchasing'` dans `marches_listes` :
|
||||
- Récupère les prix d'achat nets depuis le marché 999
|
||||
- Applique les paliers de remise du marché 999
|
||||
|
||||
## 3. Sécurité et développement
|
||||
|
||||
### 3.1 Accès base de données
|
||||
- **Obligatoire** : Utiliser la classe `Database` avec ses méthodes
|
||||
- **Interdit** : Appeler directement `$db->prepare()` sur l'objet Database
|
||||
- **Méthodes disponibles** :
|
||||
- `$db->fetchAll($sql, $params)` : Récupérer plusieurs lignes
|
||||
- `$db->fetchOne($sql, $params)` : Récupérer une ligne
|
||||
- `$db->query($sql, $params)` : Exécuter une requête
|
||||
- `$db->lastInsertId()` : Récupérer le dernier ID inséré
|
||||
|
||||
### 3.2 Protection contre les injections SQL
|
||||
- Utiliser `intval()` pour les entiers dans les requêtes non préparées
|
||||
- Utiliser `nettoie_input()` pour nettoyer les entrées utilisateur
|
||||
- Privilégier les requêtes préparées via la classe Database
|
||||
|
||||
### 3.3 Variables de sécurité
|
||||
- `$cidSafe = intval($cid)` : Version sécurisée pour les requêtes SQL
|
||||
- Attention lors de la modification de variables : recalculer ou utiliser directement `intval()`
|
||||
|
||||
## 4. Statuts des devis
|
||||
|
||||
### 4.1 Statuts principaux
|
||||
- **1** : En cours
|
||||
- **2** : Validé niveau 1
|
||||
- **3** : Validé niveau 2
|
||||
- **20** : Archivé
|
||||
|
||||
### 4.2 Réactivation des devis
|
||||
- Un devis archivé (statut 20) peut être réactivé (statut 1)
|
||||
- La réactivation est tracée dans `devis_histo`
|
||||
- Disponible selon les droits du rôle
|
||||
|
||||
## 5. Conventions de nommage
|
||||
|
||||
### 5.1 Fichiers
|
||||
- **Contrôleurs** : `c*.php` pour les standards, `cjx*.php` pour AJAX
|
||||
- **Modèles** : `m*.php`
|
||||
- **Vues** : `v*.php`
|
||||
|
||||
### 5.2 Tables de base de données
|
||||
- **Tables principales** : Nom simple (`devis`, `clients`, `produits`)
|
||||
- **Tables de référence** : Préfixe `x_` (`x_familles`, `x_statuts`)
|
||||
- **Tables système** : Préfixe `y_` (`y_pages`) ou `z_` (`z_logs`, `z_sessions`)
|
||||
|
||||
## 6. Gestion des prix et marges
|
||||
|
||||
### 6.1 Prix nets
|
||||
- `chk_prix_net = 1` : Prix non modifiable (marché hybride)
|
||||
- `chk_prix_net = 0` : Prix modifiable avec marges
|
||||
|
||||
### 6.2 Paliers de remise
|
||||
Les produits peuvent avoir jusqu'à 6 paliers de remise :
|
||||
- `prc_discount_1` avec `quantite_1`
|
||||
- `prc_discount_2` avec `quantite_2`
|
||||
- ... jusqu'à 6
|
||||
|
||||
---
|
||||
|
||||
*Document créé le 16 septembre 2025*
|
||||
*À mettre à jour au fur et à mesure des découvertes*
|
||||
282
docs/TODO.md
282
docs/TODO.md
@@ -4,15 +4,6 @@
|
||||
|
||||
### Module Devis
|
||||
|
||||
#### 6. ✅ Modifier un devis archivé (TERMINÉ - 12/09/2025)
|
||||
**Priorité**: Haute
|
||||
**Description**: Permettre la modification d'un devis archivé et son renvoi pour traitement sans nécessiter de duplication.
|
||||
**Tâches**:
|
||||
- [x] Ajouter un bouton "Réactiver" sur les devis archivés (statut 20)
|
||||
- [x] Permettre le changement de statut d'archivé vers "En cours"
|
||||
- [x] Conserver l'historique de réactivation dans `devis_histo`
|
||||
- [x] Adapter les droits selon les rôles (RR, DV, DIR-CO)
|
||||
|
||||
#### 8. Dupliquer une ligne produit
|
||||
**Priorité**: Moyenne
|
||||
**Description**: Permettre la duplication d'une ligne produit dans un même devis (utile pour les gratuités).
|
||||
@@ -36,19 +27,9 @@
|
||||
- [ ] Paginer les résultats de recherche
|
||||
- [ ] Export des résultats en Excel
|
||||
|
||||
#### 19. Gestion des contacts multiples
|
||||
**Priorité**: Haute
|
||||
**Description**: Permettre la gestion de plusieurs contacts par client.
|
||||
**Tâches**:
|
||||
- [ ] Créer une table `clients_contacts`
|
||||
- [ ] Migration des contacts existants vers la nouvelle structure
|
||||
- [ ] Interface CRUD pour les contacts
|
||||
- [ ] Sélecteur de contact à la création/modification de devis
|
||||
- [ ] Historique des contacts par devis
|
||||
|
||||
#### 21. Actualisation tarifaire
|
||||
**Priorité**: Moyenne
|
||||
**Description**: Permettre l'actualisation des prix selon la dernière grille tarifaire.
|
||||
**Priorité**: Moyenne
|
||||
**Description**: Permettre l'actualisation des prix selon la dernière grille tarifaire.
|
||||
**Tâches**:
|
||||
- [ ] Ajouter un bouton "Actualiser les tarifs"
|
||||
- [ ] Comparer les prix actuels avec la grille en vigueur
|
||||
@@ -56,8 +37,73 @@
|
||||
- [ ] Recalculer automatiquement les marges
|
||||
- [ ] Tracer l'actualisation dans l'historique
|
||||
|
||||
#### 22. Marchés hybrides et onglet Mercurial
|
||||
**Priorité**: Haute
|
||||
**Description**: Ajouter un onglet "Mercurial" dans la page devis pour les marchés de type hybride, listant tous les produits du marché.
|
||||
|
||||
**Tâches - Phase 1 (Onglet Mercurial)** :
|
||||
- [x] Identifier le type de marché du devis sélectionné
|
||||
- [x] Détecter si le marché est de type "hybride"
|
||||
- [x] Ajouter un nouvel onglet "Mercurial" dans l'interface devis (au niveau de l'onglet Produits)
|
||||
- [x] Récupérer tous les produits associés au marché
|
||||
- [x] Filtrer les produits "Hors Marché 999"
|
||||
- [x] Afficher la liste des produits dans l'onglet Mercurial
|
||||
- [x] Gérer l'affichage/masquage de l'onglet selon le type de marché
|
||||
|
||||
**Tâches - Phase 2 (Améliorations visuelles et règles métier)** :
|
||||
- [ ] **Visibilité de l'onglet Mercurial** : Rendre l'onglet "Mercurial" visuellement distinct (couleur de fond différente, par exemple) pour qu'il soit clairement identifiable par les commerciaux
|
||||
|
||||
**Types de marchés hybrides** :
|
||||
Deux cas de marchés hybrides doivent être gérés différemment :
|
||||
|
||||
**CAS 1 - Mercuriale sans remise** :
|
||||
- Liste mercuriale en prix nets SANS remise applicable sur ces références
|
||||
- Reste du catalogue disponible avec possibilité de remises en autonomie
|
||||
|
||||
**CAS 2 - Mercuriale avec remise possible** :
|
||||
- Liste mercuriale en prix nets AVEC possibilité de remises sur ces références
|
||||
- Reste du catalogue disponible avec possibilité de remises en autonomie
|
||||
|
||||
**Règles communes aux 2 cas** :
|
||||
- Quand le devis contient UNIQUEMENT des références mercuriales → pas de demande d'accord nécessaire, le RR peut valider directement
|
||||
- Quand le devis contient références mercuriales + catalogue général → seules les références du catalogue sont concernées par les seuils de marge et peuvent générer une demande d'accord
|
||||
- Si geste commercial souhaité sur un devis 100% mercurial → utiliser le champ "Demande geste commercial" existant
|
||||
|
||||
**Tâches - Paramétrage base de données** :
|
||||
- [ ] Ajouter un champ dans la table `marches` pour définir le type de marché hybride :
|
||||
- `type_mercurial` (ENUM ou INT) : NULL = non hybride, 1 = CAS 1 (sans remise), 2 = CAS 2 (avec remise)
|
||||
- [ ] Modifier la fiche marché pour permettre la sélection du type de marché hybride
|
||||
|
||||
**Tâches - Logique métier de validation** :
|
||||
- [ ] Détecter si un devis contient uniquement des produits mercuriaux
|
||||
- [ ] Détecter si un devis contient un mix mercurial + catalogue
|
||||
- [ ] Adapter le calcul des seuils de marge :
|
||||
- Si devis 100% mercurial → pas de vérification de seuil, validation RR directe
|
||||
- Si devis mixte → calculer les seuils uniquement sur les produits du catalogue général
|
||||
- [ ] Bloquer/autoriser les remises sur produits mercuriaux selon le type de marché (CAS 1 vs CAS 2)
|
||||
- [ ] Tester le workflow complet avec les 2 types de marchés hybrides
|
||||
|
||||
### Module SAP
|
||||
|
||||
#### 13. Import et contrôle des clients SAP
|
||||
**Priorité**: Haute
|
||||
**Description**: Contrôler les nouveaux clients créés dans la base CLEO et vérifier la correspondance avec la base SAP.
|
||||
**Tâches**:
|
||||
- [ ] Identifier le script/contrôleur d'import des clients SAP
|
||||
- [ ] Analyser la structure des données importées
|
||||
- [ ] Mettre en place un système de contrôle de correspondance
|
||||
- [ ] Vérifier l'unicité du `clients.code` (identifiant SAP)
|
||||
- [ ] Détecter les doublons potentiels (nom, adresse)
|
||||
- [ ] Signaler les incohérences entre SAP et CLEO
|
||||
- [ ] Créer un rapport d'import avec :
|
||||
- [ ] Nombre de clients importés
|
||||
- [ ] Nombre de clients mis à jour
|
||||
- [ ] Nombre d'anomalies détectées
|
||||
- [ ] Gestion des cas particuliers :
|
||||
- [ ] Client existe dans CLEO mais pas dans SAP
|
||||
- [ ] Client existe dans SAP mais code différent dans CLEO
|
||||
- [ ] Contacts orphelins après import
|
||||
|
||||
#### 14. Gestion de la prise en charge
|
||||
**Priorité**: Haute
|
||||
**Description**: Ajouter la traçabilité de la prise en charge et du transfert EDI.
|
||||
@@ -117,65 +163,7 @@
|
||||
|
||||
### Plan de migration - État d'avancement
|
||||
|
||||
#### ✅ Phase 0 - Refactoring base de données (COMPLÉTÉ - 12/09/2025)
|
||||
- [x] Script de migration SQL créé
|
||||
- [x] Table `y_pages` migrée depuis `uof_frontal`
|
||||
- [x] Table `z_logs` créée dans `cleo`
|
||||
- [x] Base `cleo` créée avec toutes les tables
|
||||
- [x] Données migrées de `uof_linet` vers `cleo`
|
||||
- [x] Références à `uof_frontal` supprimées
|
||||
- [x] Classe Database PDO créée
|
||||
- [x] Variables d'environnement `.env` implémentées
|
||||
- [x] Tests validés en DEV
|
||||
|
||||
#### ✅ Phase 1 - Environnement DEV IN3 (COMPLÉTÉ - 12/09/2025)
|
||||
- [x] Container `maria3` créé sur IN3
|
||||
- [x] MariaDB 11.4 installé et configuré
|
||||
- [x] Base `cleo` migrée vers `maria3`
|
||||
- [x] Configuration pointant vers `maria3` (IP: 13.23.33.4)
|
||||
- [x] Application testée et fonctionnelle
|
||||
- [x] MariaDB supprimé de `dva-front`
|
||||
- [x] Script de déploiement optimisé (`deploy-cleo-fast.sh`)
|
||||
|
||||
#### Phase 2 - Préparation PROD IN4 (À FAIRE)
|
||||
**Export depuis IN3:**
|
||||
- [ ] Exporter le container `dva-front` depuis IN3
|
||||
```bash
|
||||
incus export dva-front dva-front-export.tar.gz
|
||||
```
|
||||
- [ ] Exporter le container `maria3` depuis IN3
|
||||
```bash
|
||||
incus export maria3 maria3-export.tar.gz
|
||||
```
|
||||
|
||||
**Import sur IN4:**
|
||||
- [ ] Importer `dva-front` comme `pra-front` sur IN4
|
||||
```bash
|
||||
incus import dva-front-export.tar.gz pra-front
|
||||
```
|
||||
- [ ] Importer `maria3` comme `maria4` sur IN4
|
||||
```bash
|
||||
incus import maria3-export.tar.gz maria4
|
||||
```
|
||||
- [ ] Configurer les IPs et paramètres réseau sur IN4
|
||||
- [ ] Adapter le fichier `.env` pour l'environnement PROD
|
||||
|
||||
#### Phase 3 - Migration des données PROD (À FAIRE)
|
||||
- [ ] Effectuer une sauvegarde complète des bases PROD sur IN2/nx4
|
||||
- [ ] Exporter les données de `uof_frontal` et `uof_linet` depuis IN2/nx4
|
||||
- [ ] Utiliser le script de migration SQL pour fusionner les données
|
||||
- [ ] Importer les données fusionnées dans `maria4` sur IN4
|
||||
- [ ] Configurer `pra-front` pour pointer vers `maria4`
|
||||
- [ ] Tests de validation en pré-production
|
||||
|
||||
#### Phase 4 - Bascule PROD (À FAIRE)
|
||||
- [ ] Planifier la fenêtre de maintenance
|
||||
- [ ] Arrêter l'application sur IN2
|
||||
- [ ] Synchronisation finale des données vers IN4/maria4
|
||||
- [ ] Basculer le DNS/proxy vers IN4
|
||||
- [ ] Valider le fonctionnement en production
|
||||
- [ ] Monitoring post-migration (48h)
|
||||
- [ ] Décommissionner IN2 après période de stabilisation
|
||||
✅ **Migration complétée** - Toutes les phases (0 à 4) sont terminées.
|
||||
|
||||
### Configuration technique
|
||||
|
||||
@@ -207,6 +195,81 @@ DB_PASSWORD=<PROD_PASSWORD> # À sécuriser
|
||||
- [ ] Scripts de backup automatisés à mettre en place
|
||||
- [ ] Réplication master-slave pour haute disponibilité (optionnel)
|
||||
|
||||
## Modification Contacts Clients - Migration vers clients.code
|
||||
|
||||
### Contexte
|
||||
La relation entre `clients_contacts` et `clients` utilise actuellement `clients.rowid` comme clé étrangère.
|
||||
Cela pose problème lors des imports SAP qui peuvent écraser ou modifier les `rowid`.
|
||||
Il faut migrer vers `clients.code` (identifiant SAP immuable) pour garantir l'intégrité des relations.
|
||||
|
||||
### Plan de correction
|
||||
|
||||
#### 1. Vérification préalable
|
||||
- [ ] Lire la structure actuelle de la table `clients`
|
||||
- [ ] Confirmer que `code` est de type INT
|
||||
- [ ] Vérifier la contrainte UNIQUE sur `code`
|
||||
- [ ] Vérifier l'index sur `code`
|
||||
- [ ] Lire la structure de `clients_contacts`
|
||||
- [ ] État actuel de `fk_client`
|
||||
- [ ] Contraintes de clé étrangère existantes
|
||||
- [ ] Vérifier les données existantes
|
||||
- [ ] Nombre de contacts déjà enregistrés
|
||||
- [ ] Cohérence des relations actuelles
|
||||
|
||||
#### 2. Modification de la structure
|
||||
- [ ] Supprimer la contrainte FK actuelle sur `clients_contacts.fk_client`
|
||||
- [ ] Modifier le type de `clients_contacts.fk_client` pour correspondre à `clients.code`
|
||||
- [ ] Ajouter la nouvelle contrainte FK référençant `clients.code`
|
||||
- [ ] `ON DELETE CASCADE`
|
||||
- [ ] `ON UPDATE CASCADE`
|
||||
- [ ] Vérifier/ajouter index UNIQUE sur `clients.code` si nécessaire
|
||||
|
||||
#### 3. Migration des données
|
||||
- [ ] Créer un script de migration SQL
|
||||
- [ ] Sauvegarder les données actuelles de `clients_contacts`
|
||||
- [ ] Convertir les `fk_client` (rowid → code)
|
||||
- [ ] Valider la cohérence des données migrées
|
||||
- [ ] Tester l'intégrité référentielle
|
||||
|
||||
#### 4. Adaptation du code applicatif
|
||||
- [x] ✅ Contrôleur `cjxcontacts.php`
|
||||
- Aucune modification nécessaire (utilise déjà `fk_client` de manière générique)
|
||||
- [x] ✅ Contrôleur `cjxdevis.php`
|
||||
- `load_clients_devis` : modifié pour retourner `clients.code`
|
||||
- `save_new_client` : modifié pour utiliser `newCode` au lieu de `newClientId`
|
||||
- [x] ✅ JavaScript `jdevis.js`
|
||||
- Fonction `autocompleteClient` : modifiée pour utiliser `list[i]['code']` au lieu de `list[i]['rowid']`
|
||||
- `loadContactsClient(list[i]['code'])` : passe maintenant le code SAP
|
||||
- [ ] **Import clients SAP** : À TRAITER EN PRIORITÉ
|
||||
- [ ] Fichier concerné : identifier le contrôleur/script d'import
|
||||
- [ ] Lors de l'import, si un client existe déjà (même `code`), mettre à jour ses infos SANS changer le `code`
|
||||
- [ ] Gérer la mise à jour des contacts : les contacts existants doivent conserver leur lien via `fk_client = code`
|
||||
- [ ] Si import d'un nouveau client : créer avec le `code` SAP fourni
|
||||
- [ ] IMPORTANT : Ne jamais modifier `clients.code` après création (immuable)
|
||||
|
||||
#### 5. Tests et validation
|
||||
- [ ] Tests de création de contact
|
||||
- [ ] Tests de modification de contact
|
||||
- [ ] Tests de suppression de contact (soft delete)
|
||||
- [ ] Tests de sélection de contact dans un devis
|
||||
- [ ] Simuler un import SAP et vérifier la stabilité des relations
|
||||
|
||||
### Notes techniques
|
||||
```sql
|
||||
-- Exemple de modification FK
|
||||
ALTER TABLE clients_contacts
|
||||
DROP FOREIGN KEY fk_clients_contacts_client;
|
||||
|
||||
ALTER TABLE clients_contacts
|
||||
ADD CONSTRAINT fk_clients_contacts_client
|
||||
FOREIGN KEY (fk_client)
|
||||
REFERENCES clients(code)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Améliorations techniques prioritaires
|
||||
|
||||
### Sécurité
|
||||
@@ -252,26 +315,15 @@ DB_PASSWORD=<PROD_PASSWORD> # À sécuriser
|
||||
|
||||
## Notes de développement
|
||||
|
||||
### Structure de la table `clients_contacts` (à créer)
|
||||
```sql
|
||||
CREATE TABLE clients_contacts (
|
||||
rowid INT PRIMARY KEY AUTO_INCREMENT,
|
||||
fk_client INT NOT NULL,
|
||||
nom VARCHAR(100),
|
||||
prenom VARCHAR(100),
|
||||
fonction VARCHAR(100),
|
||||
telephone VARCHAR(20),
|
||||
mobile VARCHAR(20),
|
||||
email VARCHAR(255),
|
||||
principal TINYINT DEFAULT 0,
|
||||
active TINYINT DEFAULT 1,
|
||||
date_creat DATETIME,
|
||||
fk_user_creat INT,
|
||||
date_modif DATETIME,
|
||||
fk_user_modif INT,
|
||||
FOREIGN KEY (fk_client) REFERENCES clients(rowid)
|
||||
);
|
||||
```
|
||||
### ✅ Structure de la table `clients_contacts` (CRÉÉE - v2.0.3)
|
||||
Table créée et opérationnelle avec :
|
||||
- Clé étrangère vers `clients` avec CASCADE
|
||||
- Gestion du contact principal (un seul par client)
|
||||
- Soft delete via champ `active`
|
||||
- Traçabilité (date_creat, fk_user_creat, date_modif, fk_user_modif)
|
||||
- Index sur fk_client, principal et email
|
||||
- Contrainte UNIQUE sur rowid
|
||||
- Voir `docs/migration_clients_contacts.sql` pour la structure complète
|
||||
|
||||
### Modifications table `devis` pour SAP
|
||||
```sql
|
||||
@@ -285,24 +337,36 @@ ALTER TABLE devis ADD COLUMN erreur_transfert_edi TEXT;
|
||||
|
||||
## Résumé de l'état actuel
|
||||
|
||||
### ✅ Réalisations (v2.0.2 - 12 septembre 2025)
|
||||
### ✅ Réalisations
|
||||
**v2.0.1 (12 septembre 2025)**
|
||||
1. **Migration DEV complétée** : Architecture séparée application/BDD
|
||||
2. **Base unique `cleo`** : Fusion réussie de 3 bases en une seule
|
||||
3. **Sécurité renforcée** : PDO, requêtes préparées, variables d'environnement
|
||||
4. **Container `dva-front`** : MariaDB supprimé, application PHP uniquement
|
||||
5. **Container `maria3`** : Base de données centralisée opérationnelle
|
||||
6. **Audit de sécurité complété** : 14 vulnérabilités SQL identifiées et corrigées
|
||||
|
||||
**v2.0.2 (12 septembre 2025)**
|
||||
1. **Audit de sécurité complété** : 14 vulnérabilités SQL identifiées et corrigées
|
||||
- 8 critiques (fonction autocomplete, injections dans cjxpost.php, mclients.php, mdevis.php)
|
||||
- 6 moyennes (cjxdevis.php, cjxexport.php, cjximport.php, mexpxls.php)
|
||||
7. **Fonctionnalité Réactivation devis** : Bouton permettant de réactiver les devis archivés (statut 20 → 1)
|
||||
2. **Fonctionnalité Réactivation devis** : Bouton permettant de réactiver les devis archivés (statut 20 → 1)
|
||||
|
||||
**v2.0.3 (21 octobre 2025)**
|
||||
1. **Gestion multi-contacts par client** : Table `clients_contacts` opérationnelle
|
||||
2. **Interface CRUD complète** : Modale Bootstrap avec création/modification/suppression de contacts
|
||||
3. **Contrôleur AJAX `cjxcontacts.php`** : 5 endpoints sécurisés avec requêtes préparées
|
||||
4. **Intégration dans les devis** : Sélecteur de contact avec affichage des infos en lecture seule
|
||||
5. **Gestion automatique du contact principal** : Un seul contact principal par client
|
||||
6. **Soft delete** : Prévention de la suppression du dernier contact actif
|
||||
|
||||
### 🎯 Prochaines étapes prioritaires
|
||||
1. **Migration PROD vers IN4** : Export/Import des containers vers `pra-front` et `maria4`
|
||||
2. **Fonctionnalités métier** : Points 14, 16 (voir sections ci-dessus)
|
||||
3. **Sécurité XSS** : Audit et correction des failles XSS potentielles
|
||||
4. **Tests** : Mise en place de tests automatisés de sécurité
|
||||
1. **Nettoyage BDD** : Supprimer les anciens champs contact de la table `clients` (après validation)
|
||||
2. **Migration PROD vers IN4** : Export/Import des containers vers `pra-front` et `maria4`
|
||||
3. **Fonctionnalités métier** : Points 8, 14, 16, 21 (voir sections ci-dessus)
|
||||
4. **Sécurité XSS** : Audit et correction des failles XSS potentielles
|
||||
5. **Tests** : Mise en place de tests automatisés de sécurité
|
||||
|
||||
---
|
||||
|
||||
*Document mis à jour le 12 septembre 2025*
|
||||
*Version 2.0.2 - Sécurité SQL complète*
|
||||
*Document mis à jour le 21 octobre 2025*
|
||||
*Version 2.0.3 - Gestion multi-contacts*
|
||||
440
docs/backpm7.sh
Normal file
440
docs/backpm7.sh
Normal file
@@ -0,0 +1,440 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -uo pipefail
|
||||
# Note: Removed -e to allow script to continue on errors
|
||||
# Errors are handled explicitly with ERROR_COUNT
|
||||
|
||||
# Parse command line arguments
|
||||
ONLY_DB=false
|
||||
if [[ "${1:-}" == "-onlydb" ]]; then
|
||||
ONLY_DB=true
|
||||
echo "Mode: Database backup only"
|
||||
fi
|
||||
|
||||
# Configuration
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CONFIG_FILE="$SCRIPT_DIR/backpm7.yaml"
|
||||
LOG_DIR="$SCRIPT_DIR/logs"
|
||||
mkdir -p "$LOG_DIR"
|
||||
LOG_FILE="$LOG_DIR/backpm7-$(date +%Y%m%d).log"
|
||||
ERROR_COUNT=0
|
||||
EMAIL_TO="support@unikoffice.com"
|
||||
RECAP_FILE="/tmp/backup_recap_$$.txt"
|
||||
|
||||
# Clean old log files (keep only last 10)
|
||||
find "$LOG_DIR" -maxdepth 1 -name "backpm7-*.log" -type f 2>/dev/null | sort -r | tail -n +11 | xargs -r rm -f || true
|
||||
|
||||
# Check dependencies - COMMENTED OUT
|
||||
# for cmd in yq ssh tar openssl; do
|
||||
# if ! command -v "$cmd" &> /dev/null; then
|
||||
# echo "ERROR: $cmd is required but not installed" | tee -a "$LOG_FILE"
|
||||
# exit 1
|
||||
# fi
|
||||
# done
|
||||
|
||||
# Load config
|
||||
DIR_BACKUP=$(yq '.global.dir_backup' "$CONFIG_FILE" | tr -d '"')
|
||||
ENC_KEY_PATH=$(yq '.global.enc_key' "$CONFIG_FILE" | tr -d '"')
|
||||
|
||||
# Load encryption key
|
||||
if [[ ! -f "$ENC_KEY_PATH" ]]; then
|
||||
echo "ERROR: Encryption key not found: $ENC_KEY_PATH" | tee -a "$LOG_FILE"
|
||||
exit 1
|
||||
fi
|
||||
ENC_KEY=$(cat "$ENC_KEY_PATH")
|
||||
|
||||
echo "=== Backup Started $(date) ===" | tee -a "$LOG_FILE"
|
||||
echo "Backup directory: $DIR_BACKUP" | tee -a "$LOG_FILE"
|
||||
|
||||
# Check available disk space
|
||||
DISK_USAGE=$(df "$DIR_BACKUP" | tail -1 | awk '{print $5}' | sed 's/%//')
|
||||
DISK_FREE=$((100 - DISK_USAGE))
|
||||
|
||||
if [[ $DISK_FREE -lt 20 ]]; then
|
||||
echo "WARNING: Low disk space! Only ${DISK_FREE}% free on backup partition" | tee -a "$LOG_FILE"
|
||||
|
||||
# Send warning email
|
||||
echo "Sending DISK SPACE WARNING email to $EMAIL_TO (${DISK_FREE}% free)" | tee -a "$LOG_FILE"
|
||||
if command -v msmtp &> /dev/null; then
|
||||
{
|
||||
echo "To: $EMAIL_TO"
|
||||
echo "Subject: BackupPM7 WARNING - Low disk space (${DISK_FREE}% free)"
|
||||
echo ""
|
||||
echo "WARNING: Low disk space on $(hostname)"
|
||||
echo ""
|
||||
echo "Backup directory: $DIR_BACKUP"
|
||||
echo "Disk usage: ${DISK_USAGE}%"
|
||||
echo "Free space: ${DISK_FREE}%"
|
||||
echo ""
|
||||
echo "The backup will continue but please free up some space soon."
|
||||
echo ""
|
||||
echo "Date: $(date '+%d.%m.%Y %H:%M')"
|
||||
} | msmtp "$EMAIL_TO"
|
||||
echo "DISK SPACE WARNING email sent successfully to $EMAIL_TO" | tee -a "$LOG_FILE"
|
||||
else
|
||||
echo "WARNING: msmtp not found - DISK WARNING email NOT sent" | tee -a "$LOG_FILE"
|
||||
fi
|
||||
else
|
||||
echo "Disk space OK: ${DISK_FREE}% free" | tee -a "$LOG_FILE"
|
||||
fi
|
||||
|
||||
# Initialize recap file
|
||||
echo "BACKUP REPORT - $(hostname) - $(date '+%d.%m.%Y %H')h" > "$RECAP_FILE"
|
||||
echo "========================================" >> "$RECAP_FILE"
|
||||
echo "" >> "$RECAP_FILE"
|
||||
|
||||
# Function to format size in MB with thousand separator
|
||||
format_size_mb() {
|
||||
local file="$1"
|
||||
if [[ -f "$file" ]]; then
|
||||
local size_kb=$(du -k "$file" | cut -f1)
|
||||
local size_mb=$((size_kb / 1024))
|
||||
# Add thousand separator with printf and sed
|
||||
printf "%d" "$size_mb" | sed ':a;s/\B[0-9]\{3\}\>/\.&/;ta'
|
||||
else
|
||||
echo "0"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to backup a single database (must be defined before use)
|
||||
backup_database() {
|
||||
local database="$1"
|
||||
local backup_file="$backup_dir/sql/${database}_$(date +%Y%m%d_%H).sql.gz.enc"
|
||||
|
||||
echo " Backing up database: $database" | tee -a "$LOG_FILE"
|
||||
|
||||
if [[ "$ssh_user" != "root" ]]; then
|
||||
CMD_PREFIX="sudo"
|
||||
else
|
||||
CMD_PREFIX=""
|
||||
fi
|
||||
|
||||
# Execute backup with encryption
|
||||
# First test MySQL connection to get clear error messages (|| true to continue on error)
|
||||
MYSQL_TEST=$(ssh -i "$ssh_key" -p "$ssh_port" -o ConnectTimeout=20 "$ssh_user@$host_ip" \
|
||||
"$CMD_PREFIX incus exec $container_name -- mariadb -h $db_host -u$db_user -p$db_pass -e 'SELECT 1' 2>&1" 2>/dev/null || true)
|
||||
|
||||
if ssh -i "$ssh_key" -p "$ssh_port" -o ConnectTimeout=20 "$ssh_user@$host_ip" \
|
||||
"$CMD_PREFIX incus exec $container_name -- bash -c 'mariadb-dump -h $db_host -u$db_user -p$db_pass --add-drop-table --create-options --databases $database 2>/dev/null | gzip'" | \
|
||||
openssl enc -aes-256-cbc -salt -pass pass:"$ENC_KEY" -pbkdf2 > "$backup_file" 2>/dev/null; then
|
||||
|
||||
# Validate backup file size (encrypted SQL should be > 100 bytes)
|
||||
if [[ -f "$backup_file" ]]; then
|
||||
file_size=$(stat -c%s "$backup_file" 2>/dev/null || echo 0)
|
||||
if [[ $file_size -lt 100 ]]; then
|
||||
# Analyze MySQL connection test results
|
||||
if [[ "$MYSQL_TEST" == *"Access denied"* ]]; then
|
||||
echo " ERROR: MySQL authentication failed for $database on $host_name/$container_name" | tee -a "$LOG_FILE"
|
||||
echo " User: $db_user@$db_host - Check password in configuration" | tee -a "$LOG_FILE"
|
||||
elif [[ "$MYSQL_TEST" == *"Unknown database"* ]]; then
|
||||
echo " ERROR: Database '$database' does not exist on $host_name/$container_name" | tee -a "$LOG_FILE"
|
||||
elif [[ "$MYSQL_TEST" == *"Can't connect"* ]]; then
|
||||
echo " ERROR: Cannot connect to MySQL server at $db_host in $container_name" | tee -a "$LOG_FILE"
|
||||
else
|
||||
echo " ERROR: Backup file too small (${file_size} bytes): $database on $host_name/$container_name" | tee -a "$LOG_FILE"
|
||||
fi
|
||||
|
||||
((ERROR_COUNT++))
|
||||
rm -f "$backup_file"
|
||||
else
|
||||
size=$(du -h "$backup_file" | cut -f1)
|
||||
size_mb=$(format_size_mb "$backup_file")
|
||||
echo " ✓ Saved (encrypted): $(basename "$backup_file") ($size)" | tee -a "$LOG_FILE"
|
||||
echo " SQL: $(basename "$backup_file") - ${size_mb} Mo" >> "$RECAP_FILE"
|
||||
fi
|
||||
else
|
||||
echo " ERROR: Backup file not created: $database" | tee -a "$LOG_FILE"
|
||||
((ERROR_COUNT++))
|
||||
fi
|
||||
else
|
||||
# Analyze MySQL connection test for failed backup
|
||||
if [[ "$MYSQL_TEST" == *"Access denied"* ]]; then
|
||||
echo " ERROR: MySQL authentication failed for $database on $host_name/$container_name" | tee -a "$LOG_FILE"
|
||||
echo " User: $db_user@$db_host - Check password in configuration" | tee -a "$LOG_FILE"
|
||||
elif [[ "$MYSQL_TEST" == *"Unknown database"* ]]; then
|
||||
echo " ERROR: Database '$database' does not exist on $host_name/$container_name" | tee -a "$LOG_FILE"
|
||||
elif [[ "$MYSQL_TEST" == *"Can't connect"* ]]; then
|
||||
echo " ERROR: Cannot connect to MySQL server at $db_host in $container_name" | tee -a "$LOG_FILE"
|
||||
else
|
||||
echo " ERROR: Failed to backup database $database on $host_name/$container_name" | tee -a "$LOG_FILE"
|
||||
fi
|
||||
|
||||
((ERROR_COUNT++))
|
||||
rm -f "$backup_file"
|
||||
fi
|
||||
}
|
||||
|
||||
# Process each host
|
||||
host_count=$(yq '.hosts | length' "$CONFIG_FILE")
|
||||
|
||||
for ((i=0; i<$host_count; i++)); do
|
||||
host_name=$(yq ".hosts[$i].name" "$CONFIG_FILE" | tr -d '"')
|
||||
host_ip=$(yq ".hosts[$i].ip" "$CONFIG_FILE" | tr -d '"')
|
||||
ssh_user=$(yq ".hosts[$i].user" "$CONFIG_FILE" | tr -d '"')
|
||||
ssh_key=$(yq ".hosts[$i].key" "$CONFIG_FILE" | tr -d '"')
|
||||
ssh_port=$(yq ".hosts[$i].port // 22" "$CONFIG_FILE" | tr -d '"')
|
||||
|
||||
echo "Processing host: $host_name ($host_ip)" | tee -a "$LOG_FILE"
|
||||
echo "" >> "$RECAP_FILE"
|
||||
echo "HOST: $host_name ($host_ip)" >> "$RECAP_FILE"
|
||||
echo "----------------------------" >> "$RECAP_FILE"
|
||||
|
||||
# Test SSH connection
|
||||
if ! ssh -i "$ssh_key" -p "$ssh_port" -o ConnectTimeout=20 -o StrictHostKeyChecking=no "$ssh_user@$host_ip" "true" 2>/dev/null; then
|
||||
echo " ERROR: Cannot connect to $host_name ($host_ip)" | tee -a "$LOG_FILE"
|
||||
((ERROR_COUNT++))
|
||||
continue
|
||||
fi
|
||||
|
||||
# Process containers
|
||||
container_count=$(yq ".hosts[$i].containers | length" "$CONFIG_FILE" 2>/dev/null || echo "0")
|
||||
|
||||
for ((c=0; c<$container_count; c++)); do
|
||||
container_name=$(yq ".hosts[$i].containers[$c].name" "$CONFIG_FILE" | tr -d '"')
|
||||
|
||||
echo " Processing container: $container_name" | tee -a "$LOG_FILE"
|
||||
|
||||
# Add container to recap
|
||||
echo " Container: $container_name" >> "$RECAP_FILE"
|
||||
|
||||
# Create backup directories
|
||||
backup_dir="$DIR_BACKUP/$host_name/$container_name"
|
||||
mkdir -p "$backup_dir"
|
||||
mkdir -p "$backup_dir/sql"
|
||||
|
||||
# Backup directories (skip if -onlydb mode)
|
||||
if [[ "$ONLY_DB" == "false" ]]; then
|
||||
dir_count=$(yq ".hosts[$i].containers[$c].dirs | length" "$CONFIG_FILE" 2>/dev/null || echo "0")
|
||||
|
||||
for ((d=0; d<$dir_count; d++)); do
|
||||
dir_path=$(yq ".hosts[$i].containers[$c].dirs[$d]" "$CONFIG_FILE" | sed 's/^"\|"$//g')
|
||||
|
||||
# Use sudo if not root
|
||||
if [[ "$ssh_user" != "root" ]]; then
|
||||
CMD_PREFIX="sudo"
|
||||
else
|
||||
CMD_PREFIX=""
|
||||
fi
|
||||
|
||||
# Special handling for /var/www - backup each subdirectory separately
|
||||
if [[ "$dir_path" == "/var/www" ]]; then
|
||||
echo " Backing up subdirectories of $dir_path" | tee -a "$LOG_FILE"
|
||||
|
||||
# Get list of subdirectories
|
||||
subdirs=$(ssh -i "$ssh_key" -p "$ssh_port" -o ConnectTimeout=20 "$ssh_user@$host_ip" \
|
||||
"$CMD_PREFIX incus exec $container_name -- find /var/www -maxdepth 1 -type d ! -path /var/www" 2>/dev/null || echo "")
|
||||
|
||||
for subdir in $subdirs; do
|
||||
subdir_name=$(basename "$subdir" | tr '/' '_')
|
||||
backup_file="$backup_dir/www_${subdir_name}_$(date +%Y%m%d_%H).tar.gz"
|
||||
|
||||
echo " Backing up: $subdir" | tee -a "$LOG_FILE"
|
||||
|
||||
if ssh -i "$ssh_key" -p "$ssh_port" -o ConnectTimeout=20 "$ssh_user@$host_ip" \
|
||||
"$CMD_PREFIX incus exec $container_name -- tar czf - $subdir 2>/dev/null" > "$backup_file"; then
|
||||
|
||||
# Validate backup file size (tar.gz should be > 1KB)
|
||||
if [[ -f "$backup_file" ]]; then
|
||||
file_size=$(stat -c%s "$backup_file" 2>/dev/null || echo 0)
|
||||
if [[ $file_size -lt 1024 ]]; then
|
||||
echo " WARNING: Backup file very small (${file_size} bytes): $subdir" | tee -a "$LOG_FILE"
|
||||
# Keep the file but note it's small
|
||||
size=$(du -h "$backup_file" | cut -f1)
|
||||
size_mb=$(format_size_mb "$backup_file")
|
||||
echo " ✓ Saved (small): $(basename "$backup_file") ($size)" | tee -a "$LOG_FILE"
|
||||
echo " DIR: $(basename "$backup_file") - ${size_mb} Mo (WARNING: small)" >> "$RECAP_FILE"
|
||||
else
|
||||
size=$(du -h "$backup_file" | cut -f1)
|
||||
size_mb=$(format_size_mb "$backup_file")
|
||||
echo " ✓ Saved: $(basename "$backup_file") ($size)" | tee -a "$LOG_FILE"
|
||||
echo " DIR: $(basename "$backup_file") - ${size_mb} Mo" >> "$RECAP_FILE"
|
||||
fi
|
||||
else
|
||||
echo " ERROR: Backup file not created: $subdir" | tee -a "$LOG_FILE"
|
||||
((ERROR_COUNT++))
|
||||
fi
|
||||
else
|
||||
echo " ERROR: Failed to backup $subdir" | tee -a "$LOG_FILE"
|
||||
((ERROR_COUNT++))
|
||||
rm -f "$backup_file"
|
||||
fi
|
||||
done
|
||||
else
|
||||
# Normal backup for other directories
|
||||
dir_name=$(basename "$dir_path" | tr '/' '_')
|
||||
backup_file="$backup_dir/${dir_name}_$(date +%Y%m%d_%H).tar.gz"
|
||||
|
||||
echo " Backing up: $dir_path" | tee -a "$LOG_FILE"
|
||||
|
||||
if ssh -i "$ssh_key" -p "$ssh_port" -o ConnectTimeout=20 "$ssh_user@$host_ip" \
|
||||
"$CMD_PREFIX incus exec $container_name -- tar czf - $dir_path 2>/dev/null" > "$backup_file"; then
|
||||
|
||||
# Validate backup file size (tar.gz should be > 1KB)
|
||||
if [[ -f "$backup_file" ]]; then
|
||||
file_size=$(stat -c%s "$backup_file" 2>/dev/null || echo 0)
|
||||
if [[ $file_size -lt 1024 ]]; then
|
||||
echo " WARNING: Backup file very small (${file_size} bytes): $dir_path" | tee -a "$LOG_FILE"
|
||||
# Keep the file but note it's small
|
||||
size=$(du -h "$backup_file" | cut -f1)
|
||||
size_mb=$(format_size_mb "$backup_file")
|
||||
echo " ✓ Saved (small): $(basename "$backup_file") ($size)" | tee -a "$LOG_FILE"
|
||||
echo " DIR: $(basename "$backup_file") - ${size_mb} Mo (WARNING: small)" >> "$RECAP_FILE"
|
||||
else
|
||||
size=$(du -h "$backup_file" | cut -f1)
|
||||
size_mb=$(format_size_mb "$backup_file")
|
||||
echo " ✓ Saved: $(basename "$backup_file") ($size)" | tee -a "$LOG_FILE"
|
||||
echo " DIR: $(basename "$backup_file") - ${size_mb} Mo" >> "$RECAP_FILE"
|
||||
fi
|
||||
else
|
||||
echo " ERROR: Backup file not created: $dir_path" | tee -a "$LOG_FILE"
|
||||
((ERROR_COUNT++))
|
||||
fi
|
||||
else
|
||||
echo " ERROR: Failed to backup $dir_path" | tee -a "$LOG_FILE"
|
||||
((ERROR_COUNT++))
|
||||
rm -f "$backup_file"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi # End of directory backup section
|
||||
|
||||
# Backup databases
|
||||
db_user=$(yq ".hosts[$i].containers[$c].db_user" "$CONFIG_FILE" 2>/dev/null | tr -d '"')
|
||||
db_pass=$(yq ".hosts[$i].containers[$c].db_pass" "$CONFIG_FILE" 2>/dev/null | tr -d '"')
|
||||
db_host=$(yq ".hosts[$i].containers[$c].db_host // \"localhost\"" "$CONFIG_FILE" 2>/dev/null | tr -d '"')
|
||||
|
||||
# Check if we're in onlydb mode
|
||||
if [[ "$ONLY_DB" == "true" ]]; then
|
||||
# Use onlydb list if it exists
|
||||
onlydb_count=$(yq ".hosts[$i].containers[$c].onlydb | length" "$CONFIG_FILE" 2>/dev/null || echo "0")
|
||||
if [[ "$onlydb_count" != "0" ]] && [[ "$onlydb_count" != "null" ]]; then
|
||||
db_count="$onlydb_count"
|
||||
use_onlydb=true
|
||||
else
|
||||
# No onlydb list, skip this container in onlydb mode
|
||||
continue
|
||||
fi
|
||||
else
|
||||
# Normal mode - use databases list
|
||||
db_count=$(yq ".hosts[$i].containers[$c].databases | length" "$CONFIG_FILE" 2>/dev/null || echo "0")
|
||||
use_onlydb=false
|
||||
fi
|
||||
|
||||
if [[ -n "$db_user" ]] && [[ -n "$db_pass" ]] && [[ "$db_count" != "0" ]]; then
|
||||
for ((db=0; db<$db_count; db++)); do
|
||||
if [[ "$use_onlydb" == "true" ]]; then
|
||||
db_name=$(yq ".hosts[$i].containers[$c].onlydb[$db]" "$CONFIG_FILE" | tr -d '"')
|
||||
else
|
||||
db_name=$(yq ".hosts[$i].containers[$c].databases[$db]" "$CONFIG_FILE" | tr -d '"')
|
||||
fi
|
||||
|
||||
if [[ "$db_name" == "ALL" ]]; then
|
||||
echo " Fetching all databases..." | tee -a "$LOG_FILE"
|
||||
|
||||
# Get database list
|
||||
if [[ "$ssh_user" != "root" ]]; then
|
||||
db_list=$(ssh -i "$ssh_key" -p "$ssh_port" -o ConnectTimeout=20 "$ssh_user@$host_ip" \
|
||||
"sudo incus exec $container_name -- mariadb -h $db_host -u$db_user -p$db_pass -e 'SHOW DATABASES;' 2>/dev/null" | \
|
||||
grep -Ev '^(Database|information_schema|performance_schema|mysql|sys)$' || echo "")
|
||||
else
|
||||
db_list=$(ssh -i "$ssh_key" -p "$ssh_port" -o ConnectTimeout=20 "$ssh_user@$host_ip" \
|
||||
"incus exec $container_name -- mariadb -h $db_host -u$db_user -p$db_pass -e 'SHOW DATABASES;' 2>/dev/null" | \
|
||||
grep -Ev '^(Database|information_schema|performance_schema|mysql|sys)$' || echo "")
|
||||
fi
|
||||
|
||||
# Backup each database
|
||||
for single_db in $db_list; do
|
||||
backup_database "$single_db"
|
||||
done
|
||||
else
|
||||
backup_database "$db_name"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
echo "=== Backup Completed $(date) ===" | tee -a "$LOG_FILE"
|
||||
|
||||
# Show summary
|
||||
total_size=$(du -sh "$DIR_BACKUP" 2>/dev/null | cut -f1)
|
||||
echo "Total backup size: $total_size" | tee -a "$LOG_FILE"
|
||||
|
||||
# Add summary to recap
|
||||
echo "" >> "$RECAP_FILE"
|
||||
echo "========================================" >> "$RECAP_FILE"
|
||||
|
||||
# Add size details per host/container
|
||||
echo "BACKUP SIZES:" >> "$RECAP_FILE"
|
||||
for host_dir in "$DIR_BACKUP"/*; do
|
||||
if [[ -d "$host_dir" ]]; then
|
||||
host_name=$(basename "$host_dir")
|
||||
host_size=$(du -sh "$host_dir" 2>/dev/null | cut -f1)
|
||||
echo " $host_name: $host_size" >> "$RECAP_FILE"
|
||||
|
||||
# Size per container
|
||||
for container_dir in "$host_dir"/*; do
|
||||
if [[ -d "$container_dir" ]]; then
|
||||
container_name=$(basename "$container_dir")
|
||||
container_size=$(du -sh "$container_dir" 2>/dev/null | cut -f1)
|
||||
echo " - $container_name: $container_size" >> "$RECAP_FILE"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done
|
||||
|
||||
echo "" >> "$RECAP_FILE"
|
||||
echo "TOTAL SIZE: $total_size" >> "$RECAP_FILE"
|
||||
echo "COMPLETED: $(date '+%d.%m.%Y %H:%M')" >> "$RECAP_FILE"
|
||||
|
||||
# Prepare email subject with date format
|
||||
DATE_SUBJECT=$(date '+%d.%m.%Y %H')
|
||||
|
||||
# Send recap email
|
||||
if [[ $ERROR_COUNT -gt 0 ]]; then
|
||||
echo "Total errors: $ERROR_COUNT" | tee -a "$LOG_FILE"
|
||||
|
||||
# Add errors to recap
|
||||
echo "" >> "$RECAP_FILE"
|
||||
echo "ERRORS DETECTED: $ERROR_COUNT" >> "$RECAP_FILE"
|
||||
echo "----------------------------" >> "$RECAP_FILE"
|
||||
grep -i "ERROR" "$LOG_FILE" >> "$RECAP_FILE"
|
||||
|
||||
# Send email with ERROR in subject
|
||||
echo "Sending ERROR email to $EMAIL_TO (Errors found: $ERROR_COUNT)" | tee -a "$LOG_FILE"
|
||||
if command -v msmtp &> /dev/null; then
|
||||
{
|
||||
echo "To: $EMAIL_TO"
|
||||
echo "Subject: BackupPM7 ERROR $DATE_SUBJECT"
|
||||
echo ""
|
||||
cat "$RECAP_FILE"
|
||||
} | msmtp "$EMAIL_TO"
|
||||
echo "ERROR email sent successfully to $EMAIL_TO" | tee -a "$LOG_FILE"
|
||||
else
|
||||
echo "WARNING: msmtp not found - ERROR email NOT sent" | tee -a "$LOG_FILE"
|
||||
fi
|
||||
else
|
||||
echo "Backup completed successfully with no errors" | tee -a "$LOG_FILE"
|
||||
|
||||
# Send success recap email
|
||||
echo "Sending SUCCESS recap email to $EMAIL_TO" | tee -a "$LOG_FILE"
|
||||
if command -v msmtp &> /dev/null; then
|
||||
{
|
||||
echo "To: $EMAIL_TO"
|
||||
echo "Subject: BackupPM7 $DATE_SUBJECT"
|
||||
echo ""
|
||||
cat "$RECAP_FILE"
|
||||
} | msmtp "$EMAIL_TO"
|
||||
echo "SUCCESS recap email sent successfully to $EMAIL_TO" | tee -a "$LOG_FILE"
|
||||
else
|
||||
echo "WARNING: msmtp not found - SUCCESS recap email NOT sent" | tee -a "$LOG_FILE"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Clean up recap file
|
||||
rm -f "$RECAP_FILE"
|
||||
|
||||
# Exit with error code if there were errors
|
||||
if [[ $ERROR_COUNT -gt 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
8977
docs/listes tarifaires générales_160725.csv
Normal file
8977
docs/listes tarifaires générales_160725.csv
Normal file
File diff suppressed because it is too large
Load Diff
143
docs/migration_clients_contacts.sql
Normal file
143
docs/migration_clients_contacts.sql
Normal file
@@ -0,0 +1,143 @@
|
||||
-- ============================================================================
|
||||
-- MIGRATION: Gestion multi-contacts par client
|
||||
-- Version: 2.0.3
|
||||
-- Date: 2025-10-21
|
||||
--
|
||||
-- Description:
|
||||
-- - Création de la table clients_contacts
|
||||
-- - Migration des contacts existants depuis la table clients
|
||||
-- - Ajout du champ fk_contact dans la table devis
|
||||
--
|
||||
-- IMPORTANT: Ce script ne modifie PAS la table clients (champs conservés)
|
||||
-- ============================================================================
|
||||
|
||||
USE cleo;
|
||||
|
||||
-- ============================================================================
|
||||
-- ÉTAPE 1: Création de la table clients_contacts
|
||||
-- ============================================================================
|
||||
|
||||
DROP TABLE IF EXISTS `clients_contacts`;
|
||||
|
||||
CREATE TABLE `clients_contacts` (
|
||||
`rowid` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`fk_client` int(11) NOT NULL,
|
||||
`nom` varchar(50) DEFAULT NULL,
|
||||
`prenom` varchar(50) DEFAULT NULL,
|
||||
`fonction` varchar(50) DEFAULT NULL,
|
||||
`telephone` varchar(20) DEFAULT NULL,
|
||||
`mobile` varchar(20) DEFAULT NULL,
|
||||
`email` varchar(75) DEFAULT NULL,
|
||||
`principal` tinyint(1) DEFAULT 0 COMMENT 'Contact principal du client',
|
||||
`active` tinyint(1) DEFAULT 1,
|
||||
`date_creat` datetime DEFAULT NULL,
|
||||
`fk_user_creat` int(11) DEFAULT NULL,
|
||||
`date_modif` datetime DEFAULT NULL,
|
||||
`fk_user_modif` int(11) DEFAULT NULL,
|
||||
PRIMARY KEY (`rowid`),
|
||||
UNIQUE KEY `rowid_UNIQUE` (`rowid`),
|
||||
KEY `fk_client` (`fk_client`),
|
||||
KEY `principal` (`fk_client`, `principal`),
|
||||
KEY `email` (`email`),
|
||||
CONSTRAINT `clients_contacts_fk_client` FOREIGN KEY (`fk_client`) REFERENCES `clients` (`rowid`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='Contacts multiples par client' `PAGE_COMPRESSED`='ON';
|
||||
|
||||
-- ============================================================================
|
||||
-- ÉTAPE 2: Migration des contacts existants depuis la table clients
|
||||
-- ============================================================================
|
||||
|
||||
INSERT INTO `clients_contacts` (
|
||||
`fk_client`,
|
||||
`nom`,
|
||||
`prenom`,
|
||||
`fonction`,
|
||||
`telephone`,
|
||||
`mobile`,
|
||||
`email`,
|
||||
`principal`,
|
||||
`active`,
|
||||
`date_creat`,
|
||||
`fk_user_creat`,
|
||||
`date_modif`,
|
||||
`fk_user_modif`
|
||||
)
|
||||
SELECT
|
||||
c.rowid AS fk_client,
|
||||
c.contact_nom AS nom,
|
||||
c.contact_prenom AS prenom,
|
||||
c.contact_fonction AS fonction,
|
||||
c.telephone,
|
||||
c.mobile,
|
||||
c.email,
|
||||
1 AS principal,
|
||||
c.active,
|
||||
c.date_creat,
|
||||
c.fk_user_creat,
|
||||
c.date_modif,
|
||||
c.fk_user_modif
|
||||
FROM `clients` c
|
||||
WHERE c.active = 1
|
||||
AND (
|
||||
c.contact_nom IS NOT NULL
|
||||
OR c.contact_prenom IS NOT NULL
|
||||
OR c.email IS NOT NULL
|
||||
OR c.telephone IS NOT NULL
|
||||
OR c.mobile IS NOT NULL
|
||||
);
|
||||
|
||||
-- ============================================================================
|
||||
-- ÉTAPE 3: Ajout du champ fk_contact dans la table devis
|
||||
-- ============================================================================
|
||||
|
||||
ALTER TABLE `devis`
|
||||
ADD COLUMN `fk_contact` int(11) DEFAULT NULL AFTER `fk_client`,
|
||||
ADD KEY `fk_contact` (`fk_contact`);
|
||||
|
||||
-- ============================================================================
|
||||
-- ÉTAPE 4: Liaison des devis existants avec les contacts principaux
|
||||
-- ============================================================================
|
||||
|
||||
UPDATE `devis` d
|
||||
INNER JOIN `clients_contacts` cc ON d.fk_client = cc.fk_client AND cc.principal = 1
|
||||
SET d.fk_contact = cc.rowid
|
||||
WHERE d.fk_client > 0;
|
||||
|
||||
-- ============================================================================
|
||||
-- ÉTAPE 5: Vérifications post-migration
|
||||
-- ============================================================================
|
||||
|
||||
-- Nombre de clients avec contacts
|
||||
SELECT COUNT(*) AS 'Clients avec contacts migrés'
|
||||
FROM clients_contacts;
|
||||
|
||||
-- Nombre de clients actifs
|
||||
SELECT COUNT(*) AS 'Total clients actifs'
|
||||
FROM clients
|
||||
WHERE active = 1;
|
||||
|
||||
-- Nombre de contacts principaux
|
||||
SELECT COUNT(*) AS 'Contacts principaux'
|
||||
FROM clients_contacts
|
||||
WHERE principal = 1;
|
||||
|
||||
-- Devis avec contact associé
|
||||
SELECT COUNT(*) AS 'Devis avec contact associé'
|
||||
FROM devis
|
||||
WHERE fk_contact IS NOT NULL;
|
||||
|
||||
-- Devis sans contact (à vérifier)
|
||||
SELECT COUNT(*) AS 'Devis SANS contact (à vérifier)'
|
||||
FROM devis
|
||||
WHERE fk_client > 0 AND fk_contact IS NULL;
|
||||
|
||||
-- Clients sans contact migré (potentiellement vides)
|
||||
SELECT c.rowid, c.code, c.libelle
|
||||
FROM clients c
|
||||
LEFT JOIN clients_contacts cc ON c.rowid = cc.fk_client
|
||||
WHERE c.active = 1
|
||||
AND cc.rowid IS NULL
|
||||
LIMIT 10;
|
||||
|
||||
-- ============================================================================
|
||||
-- FIN DE LA MIGRATION
|
||||
-- ============================================================================
|
||||
177274
docs/uof_linet_20250911.sql
177274
docs/uof_linet_20250911.sql
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user