Compare commits
5 commits
feature/k8
...
18.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
278540fbaf | ||
|
|
a080586ef4 | ||
|
|
3f5c5cc038 | ||
|
|
00a940672a | ||
|
|
ece737826c |
22 changed files with 840 additions and 461 deletions
|
|
@ -1,31 +0,0 @@
|
|||
# Migration vers Odoo 18.0 - bemade_attachments_cleanup
|
||||
|
||||
## Description
|
||||
Module de nettoyage des pièces jointes obsolètes
|
||||
|
||||
## Fonctionnalités Ajoutées
|
||||
- Suppression automatique des pièces jointes non utilisées
|
||||
- Configuration des règles de nettoyage
|
||||
- Historique des suppressions
|
||||
|
||||
## Modèles et Champs Modifiés
|
||||
- ir.attachment
|
||||
- Ajout du champ cleanup_date (date)
|
||||
- Ajout du champ cleanup_reason (text)
|
||||
|
||||
## Statut Migration
|
||||
- [ ] A migrer
|
||||
- [ ] En cours
|
||||
- [ ] Migré
|
||||
|
||||
## Détails Migration
|
||||
- Vérifier si la fonctionnalité existe déjà dans Odoo 18.0
|
||||
- Analyser les impacts sur les workflows existants
|
||||
|
||||
## Actions Requises
|
||||
- [ ] Vérifier la compatibilité avec Odoo 18.0
|
||||
- [ ] Tester les fonctionnalités
|
||||
- [ ] Mettre à jour la documentation
|
||||
|
||||
## Notes
|
||||
- Ce module pourrait être remplacé par une configuration native dans Odoo 18.0
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
# Migration vers Odoo 18.0 - bemade_helpdesk_mailcow_blacklist
|
||||
|
||||
## Description
|
||||
Module d'intégration entre Helpdesk et Mailcow pour la gestion des blacklists
|
||||
|
||||
## Fonctionnalités Ajoutées
|
||||
- Synchronisation des emails blacklistés avec Mailcow
|
||||
- Gestion des règles de blocage
|
||||
- Historique des actions de blacklist
|
||||
|
||||
## Modèles et Champs Modifiés
|
||||
- helpdesk.ticket
|
||||
- Ajout du champ mailcow_blacklisted (boolean)
|
||||
- Ajout du champ mailcow_blacklist_reason (text)
|
||||
|
||||
## Statut Migration
|
||||
- [ ] A migrer
|
||||
- [ ] En cours
|
||||
- [ ] Migré
|
||||
|
||||
## Détails Migration
|
||||
- Vérifier si la fonctionnalité existe déjà dans Odoo 18.0
|
||||
- Analyser les impacts sur les workflows existants
|
||||
|
||||
## Actions Requises
|
||||
- [ ] Vérifier la compatibilité avec Odoo 18.0
|
||||
- [ ] Tester les fonctionnalités
|
||||
- [ ] Mettre à jour la documentation
|
||||
|
||||
## Notes
|
||||
- Ce module nécessite une configuration spécifique de Mailcow
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
# Migration vers Odoo 18.0 - bemade_purchase_warn_supplier_overdue
|
||||
|
||||
## Description
|
||||
Module qui ajoute des avertissements lors de la confirmation des bons de commande pour les fournisseurs ayant des factures en retard.
|
||||
|
||||
## Analyse Technique
|
||||
|
||||
### Fonctionnalités Actuelles
|
||||
1. **Avertissements Automatiques**
|
||||
- Vérification des factures en retard
|
||||
- Création d'activités mail.activity
|
||||
- Configuration par entreprise
|
||||
|
||||
2. **Modèles Modifiés**
|
||||
- `purchase.order` : Ajout de la logique d'avertissement
|
||||
- `res.company` : Configuration des avertissements
|
||||
- `res.config.settings` : Interface de configuration
|
||||
|
||||
3. **Configuration Flexible**
|
||||
- Choix des utilisateurs à notifier
|
||||
- Sélection des fournisseurs concernés
|
||||
- Paramètres par entreprise
|
||||
|
||||
### Changements dans Odoo 18.0
|
||||
|
||||
1. **Architecture Purchase/Mail**
|
||||
- Le système d'activités reste stable
|
||||
- Les bons de commande fonctionnent de la même manière
|
||||
- Les paramètres de configuration sont similaires
|
||||
|
||||
2. **Modifications Nécessaires**
|
||||
- [ ] Adapter les vues pour les nouvelles conventions
|
||||
- [ ] Vérifier la compatibilité des activités
|
||||
- [ ] Mettre à jour les attributs des vues
|
||||
|
||||
## Plan de Migration
|
||||
|
||||
### Phase 1 : Analyse et Préparation
|
||||
1. **Révision du Code**
|
||||
- [ ] Vérifier les changements dans mail.activity
|
||||
- [ ] Tester la création d'activités
|
||||
- [ ] Identifier les potentiels conflits
|
||||
|
||||
2. **Tests**
|
||||
- [ ] Créer des cas de test avec différents scénarios
|
||||
- [ ] Documenter le comportement attendu
|
||||
- [ ] Préparer des données de test
|
||||
|
||||
### Phase 2 : Migration
|
||||
1. **Mise à Jour du Code**
|
||||
- [ ] Adapter les vues XML
|
||||
- [ ] Vérifier les dépendances
|
||||
- [ ] Optimiser les requêtes si nécessaire
|
||||
|
||||
2. **Tests et Validation**
|
||||
- [ ] Tester avec différents types de factures
|
||||
- [ ] Vérifier les notifications
|
||||
- [ ] Valider les configurations
|
||||
|
||||
## État de la Migration
|
||||
En cours d'analyse - Migration simple requise
|
||||
|
||||
## Notes Importantes
|
||||
- La fonctionnalité reste pertinente dans Odoo 18.0
|
||||
- Les changements sont mineurs
|
||||
- La logique de base reste la même
|
||||
- Attention à la performance des requêtes
|
||||
|
||||
## Prochaines Étapes
|
||||
1. Valider l'approche avec l'équipe
|
||||
2. Adapter les vues pour Odoo 18.0
|
||||
3. Mettre à jour les tests
|
||||
4. Tester avec différents scénarios
|
||||
|
||||
## Notes de Version
|
||||
- Version originale: 17.0.1.0
|
||||
- Dernière analyse: 26/01/2025
|
||||
|
||||
## Points d'Attention Particuliers
|
||||
1. **Performance**
|
||||
- Optimisation des requêtes de factures
|
||||
- Gestion du cache
|
||||
- Impact sur les grands volumes
|
||||
|
||||
2. **Interface Utilisateur**
|
||||
- Clarté des avertissements
|
||||
- Configuration intuitive
|
||||
- Visibilité des notifications
|
||||
|
||||
3. **Maintenance**
|
||||
- Documentation des configurations
|
||||
- Gestion des cas spéciaux
|
||||
- Logs pour le débogage
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
# Migration vers Odoo 18.0 - bemade_pwa_config
|
||||
|
||||
## Description
|
||||
Module qui permet la configuration des paramètres PWA (Progressive Web App) directement depuis l'interface Odoo, incluant la gestion dynamique des icônes d'application.
|
||||
|
||||
## Analyse Technique
|
||||
|
||||
### Fonctionnalités Actuelles
|
||||
1. **Configuration PWA**
|
||||
- Gestion des icônes d'application
|
||||
- Configuration des couleurs
|
||||
- Génération dynamique des icônes
|
||||
|
||||
2. **Modèles Modifiés**
|
||||
- `res.company` : Stockage des configurations
|
||||
- `res.config.settings` : Interface de configuration
|
||||
- Contrôleur pour le manifest.webmanifest
|
||||
|
||||
3. **Fonctionnalités Avancées**
|
||||
- Redimensionnement automatique des icônes
|
||||
- Manifest dynamique par entreprise
|
||||
- Gestion des couleurs de thème
|
||||
|
||||
### Changements dans Odoo 18.0
|
||||
|
||||
1. **Architecture Web**
|
||||
- Le système PWA est toujours supporté
|
||||
- Les routes HTTP restent stables
|
||||
- La gestion des images est similaire
|
||||
|
||||
2. **Modifications Nécessaires**
|
||||
- [ ] Vérifier la compatibilité avec le nouveau framework web
|
||||
- [ ] Adapter les routes HTTP si nécessaire
|
||||
- [ ] Mettre à jour les dépendances PIL
|
||||
|
||||
## Plan de Migration
|
||||
|
||||
### Phase 1 : Analyse et Préparation
|
||||
1. **Révision du Code**
|
||||
- [ ] Vérifier les changements dans le framework web
|
||||
- [ ] Tester le traitement des images
|
||||
- [ ] Identifier les potentiels conflits
|
||||
|
||||
2. **Tests**
|
||||
- [ ] Créer des cas de test pour les icônes
|
||||
- [ ] Documenter le comportement attendu
|
||||
- [ ] Préparer des données de test
|
||||
|
||||
### Phase 2 : Migration
|
||||
1. **Mise à Jour du Code**
|
||||
- [ ] Adapter les vues XML
|
||||
- [ ] Vérifier les dépendances Python
|
||||
- [ ] Optimiser le traitement des images
|
||||
|
||||
2. **Tests et Validation**
|
||||
- [ ] Tester avec différentes tailles d'icônes
|
||||
- [ ] Vérifier le manifest généré
|
||||
- [ ] Valider sur différents navigateurs
|
||||
|
||||
## État de la Migration
|
||||
En cours d'analyse - Migration simple requise
|
||||
|
||||
## Notes Importantes
|
||||
- La fonctionnalité reste pertinente dans Odoo 18.0
|
||||
- Les changements sont mineurs
|
||||
- La logique de base reste la même
|
||||
- Attention à la performance du traitement d'images
|
||||
|
||||
## Prochaines Étapes
|
||||
1. Valider l'approche avec l'équipe
|
||||
2. Vérifier les changements du framework web
|
||||
3. Mettre à jour les tests
|
||||
4. Tester sur différents navigateurs
|
||||
|
||||
## Notes de Version
|
||||
- Version originale: 18.0.0.1.0
|
||||
- Dernière analyse: 26/01/2025
|
||||
|
||||
## Points d'Attention Particuliers
|
||||
1. **Performance**
|
||||
- Optimisation du traitement d'images
|
||||
- Mise en cache du manifest
|
||||
- Gestion des ressources
|
||||
|
||||
2. **Compatibilité**
|
||||
- Support des navigateurs
|
||||
- Versions de PIL/Pillow
|
||||
- Standards PWA
|
||||
|
||||
3. **Maintenance**
|
||||
- Documentation des configurations
|
||||
- Gestion des cas spéciaux
|
||||
- Logs pour le débogage
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
# Migration vers Odoo 18.0 - bemade_time_off_follower
|
||||
|
||||
## Description
|
||||
Module qui permet d'ajouter un abonné alternatif qui recevra une copie des communications pendant les congés d'un employé.
|
||||
|
||||
## Analyse Technique
|
||||
|
||||
### Fonctionnalités Actuelles
|
||||
1. **Gestion des Abonnés Alternatifs**
|
||||
- Champ pour définir l'abonné alternatif
|
||||
- Notification automatique pendant les congés
|
||||
- Gestion des destinataires des messages
|
||||
|
||||
2. **Modèles Modifiés**
|
||||
- `hr.leave` : Ajout du champ alternate_follower_id
|
||||
- `mail.thread` : Surcharge de _notify_get_recipients
|
||||
- Intégration avec le système de messagerie
|
||||
|
||||
3. **Logique de Notification**
|
||||
- Vérification des congés en cours
|
||||
- Ajout des abonnés alternatifs
|
||||
- Gestion des doublons
|
||||
|
||||
### Changements dans Odoo 18.0
|
||||
|
||||
1. **Architecture Mail/HR**
|
||||
- Le système de notification reste stable
|
||||
- Les congés fonctionnent de la même manière
|
||||
- L'API de messagerie est similaire
|
||||
|
||||
2. **Modifications Nécessaires**
|
||||
- [ ] Vérifier la compatibilité avec le nouveau système de messagerie
|
||||
- [ ] Valider la méthode _notify_get_recipients
|
||||
- [ ] Optimiser la recherche des congés
|
||||
|
||||
## Plan de Migration
|
||||
|
||||
### Phase 1 : Analyse et Préparation
|
||||
1. **Révision du Code**
|
||||
- [ ] Vérifier les changements dans l'API de messagerie
|
||||
- [ ] Tester les notifications
|
||||
- [ ] Identifier les potentiels conflits
|
||||
|
||||
2. **Tests**
|
||||
- [ ] Créer des cas de test avec différents scénarios
|
||||
- [ ] Documenter le comportement attendu
|
||||
- [ ] Préparer des données de test
|
||||
|
||||
### Phase 2 : Migration
|
||||
1. **Mise à Jour du Code**
|
||||
- [ ] Adapter le code Python
|
||||
- [ ] Vérifier les dépendances
|
||||
- [ ] Optimiser les requêtes si nécessaire
|
||||
|
||||
2. **Tests et Validation**
|
||||
- [ ] Tester avec différents types de congés
|
||||
- [ ] Vérifier les notifications
|
||||
- [ ] Valider la gestion des abonnés
|
||||
|
||||
## État de la Migration
|
||||
En cours d'analyse - Migration simple requise
|
||||
|
||||
## Notes Importantes
|
||||
- La fonctionnalité reste pertinente dans Odoo 18.0
|
||||
- Les changements sont mineurs
|
||||
- La logique de base reste la même
|
||||
- Attention à la performance des requêtes
|
||||
|
||||
## Prochaines Étapes
|
||||
1. Valider l'approche avec l'équipe
|
||||
2. Vérifier les changements dans l'API de messagerie
|
||||
3. Mettre à jour les tests
|
||||
4. Tester avec différents scénarios
|
||||
|
||||
## Notes de Version
|
||||
- Version originale: 17.0.0.0.3
|
||||
- Dernière analyse: 26/01/2025
|
||||
|
||||
## Points d'Attention Particuliers
|
||||
1. **Performance**
|
||||
- Optimisation des recherches
|
||||
- Gestion des notifications en masse
|
||||
- Impact sur les grands volumes
|
||||
|
||||
2. **Fiabilité**
|
||||
- Gestion des erreurs
|
||||
- Validation des abonnements
|
||||
- Cohérence des données
|
||||
|
||||
3. **Maintenance**
|
||||
- Documentation du comportement
|
||||
- Gestion des cas spéciaux
|
||||
- Logs pour le débogage
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
# Migration vers Odoo 18.0 - bemade_user_password_bundle
|
||||
|
||||
## Description
|
||||
Module qui automatise la création de bundles de mots de passe pour les nouveaux utilisateurs et modifie la propriété par défaut du bundle admin.
|
||||
|
||||
## Analyse Technique
|
||||
|
||||
### Fonctionnalités Actuelles
|
||||
1. **Création Automatique**
|
||||
- Bundle créé à la création d'un employé
|
||||
- Attribution des accès automatique
|
||||
- Gestion des droits d'administration
|
||||
|
||||
2. **Modèles Modifiés**
|
||||
- `hr.employee` : Surcharge de create
|
||||
- `password.bundle` : Modification des accès par défaut
|
||||
- Intégration avec odoo_password_manager
|
||||
|
||||
3. **Logique d'Accès**
|
||||
- Accès admin par défaut au groupe system
|
||||
- Accès complet pour le nouvel employé
|
||||
- Notes automatiques dans le bundle
|
||||
|
||||
### Changements dans Odoo 18.0
|
||||
|
||||
1. **Architecture Password/HR**
|
||||
- Le système de gestion des mots de passe reste stable
|
||||
- Les employés fonctionnent de la même manière
|
||||
- Les groupes de sécurité sont similaires
|
||||
|
||||
2. **Modifications Nécessaires**
|
||||
- [ ] Vérifier la compatibilité avec odoo_password_manager
|
||||
- [ ] Valider la méthode de création des bundles
|
||||
- [ ] Optimiser la gestion des accès
|
||||
|
||||
## Plan de Migration
|
||||
|
||||
### Phase 1 : Analyse et Préparation
|
||||
1. **Révision du Code**
|
||||
- [ ] Vérifier les changements dans odoo_password_manager
|
||||
- [ ] Tester la création des bundles
|
||||
- [ ] Identifier les potentiels conflits
|
||||
|
||||
2. **Tests**
|
||||
- [ ] Créer des cas de test avec différents scénarios
|
||||
- [ ] Documenter le comportement attendu
|
||||
- [ ] Préparer des données de test
|
||||
|
||||
### Phase 2 : Migration
|
||||
1. **Mise à Jour du Code**
|
||||
- [ ] Adapter le code Python
|
||||
- [ ] Vérifier les dépendances
|
||||
- [ ] Optimiser les requêtes si nécessaire
|
||||
|
||||
2. **Tests et Validation**
|
||||
- [ ] Tester avec différents types d'employés
|
||||
- [ ] Vérifier les accès
|
||||
- [ ] Valider la sécurité
|
||||
|
||||
## État de la Migration
|
||||
En cours d'analyse - Migration simple requise
|
||||
|
||||
## Notes Importantes
|
||||
- La fonctionnalité reste pertinente dans Odoo 18.0
|
||||
- Les changements sont mineurs
|
||||
- La logique de base reste la même
|
||||
- Attention à la sécurité des accès
|
||||
|
||||
## Prochaines Étapes
|
||||
1. Valider l'approche avec l'équipe
|
||||
2. Vérifier les changements dans odoo_password_manager
|
||||
3. Mettre à jour les tests
|
||||
4. Tester avec différents scénarios
|
||||
|
||||
## Notes de Version
|
||||
- Version originale: 17.0.0.1
|
||||
- Dernière analyse: 26/01/2025
|
||||
|
||||
## Points d'Attention Particuliers
|
||||
1. **Sécurité**
|
||||
- Gestion des accès
|
||||
- Protection des données
|
||||
- Audit des modifications
|
||||
|
||||
2. **Fiabilité**
|
||||
- Gestion des erreurs
|
||||
- Validation des accès
|
||||
- Cohérence des données
|
||||
|
||||
3. **Maintenance**
|
||||
- Documentation des accès
|
||||
- Gestion des cas spéciaux
|
||||
- Logs pour le débogage
|
||||
|
|
@ -223,7 +223,9 @@ class CalendarEvent(models.Model):
|
|||
{"caldav_uid": caldav_uid}
|
||||
)
|
||||
except Exception as e:
|
||||
_logger.error(f"Failed to sync event to CalDAV server: {e}")
|
||||
_logger.error(
|
||||
f"Failed to sync event to CalDAV server: {e}", exc_info=True
|
||||
)
|
||||
|
||||
def write(self, vals):
|
||||
res = super(CalendarEvent, self.with_context(caldav_no_sync=True)).write(vals)
|
||||
|
|
@ -333,8 +335,9 @@ class CalendarEvent(models.Model):
|
|||
) -> Optional[int]:
|
||||
ical_instance = caldav_event.icalendar_instance
|
||||
for index, component in enumerate(ical_instance.subcomponents):
|
||||
if component.get("name") == "VEVENT" and (
|
||||
rec_id := component.get("recurrence-id")
|
||||
if (
|
||||
component.get("name") == "VEVENT"
|
||||
and (rec_id := component.get("recurrence-id"))
|
||||
and rec_id.dt == self._get_ical_recurrence_id()
|
||||
):
|
||||
return index
|
||||
|
|
@ -347,7 +350,6 @@ class CalendarEvent(models.Model):
|
|||
calendar = client.calendar(url=user.caldav_calendar_url)
|
||||
try:
|
||||
caldav_event = calendar.event_by_uid(self.caldav_uid)
|
||||
assert isinstance(caldav_event, caldav.Event)
|
||||
if not delete_all and self.recurrence_id and not self.is_base_event:
|
||||
index = self._get_subcomponent_index_for_recurrence(
|
||||
caldav_event
|
||||
|
|
@ -488,7 +490,12 @@ class CalendarEvent(models.Model):
|
|||
|
||||
def _add_event_dates(self, event_data: Dict) -> None:
|
||||
"""Add pertinent dates to event data, based on self."""
|
||||
tz = self.event_tz or self.env.user.tz
|
||||
# Determine timezone: prefer event_tz, then user tz, finally UTC
|
||||
# Note: All datetimes in Odoo are stored in UTC, so defaulting to UTC is correct.
|
||||
# UTC times are sent in from the appointments app when installed, without
|
||||
# timezone information. This was breaking the sync process due to a call to
|
||||
# upper() on boolean value False.
|
||||
tz = self.event_tz or self.env.user.tz or "UTC"
|
||||
event_tz = timezone(tz)
|
||||
event_data["last-modified"] = vDatetime(
|
||||
utc.localize(self.write_date).astimezone(event_tz)
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
# Migration de durpro_helpdesk_sale vers Odoo 18.0
|
||||
|
||||
## Description
|
||||
Module d'intégration entre le helpdesk et les ventes pour Durpro.
|
||||
|
||||
## Fonctionnalités Ajoutées
|
||||
|
||||
### Création de commandes de vente depuis les tickets
|
||||
- Vérification Odoo 18.0 : À vérifier
|
||||
- Différences avec la version native : À documenter
|
||||
- Alternatives disponibles : À identifier
|
||||
|
||||
## Modèles et Champs Modifiés
|
||||
|
||||
### Modèle HelpdeskTicket
|
||||
- Ajouts/Modifications : À documenter
|
||||
- Recherche dans le projet : À effectuer
|
||||
- Existence dans Odoo standard/enterprise : À vérifier
|
||||
- Recommandations de migration : À formuler
|
||||
|
||||
## Vues à Modifier
|
||||
- Liste des vues tree à convertir en list : À identifier
|
||||
96
stock_inventory_adjustment_security/README.md
Normal file
96
stock_inventory_adjustment_security/README.md
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
# Stock Inventory Adjustment Security
|
||||
|
||||
## Overview
|
||||
|
||||
This module adds granular security controls for inventory adjustments in Odoo. It allows you to separate the ability to **count inventory** from the ability to **apply adjustments**.
|
||||
|
||||
## Features
|
||||
|
||||
- **New Security Group**: "Inventory: Manual Adjustments"
|
||||
- **Separation of Duties**:
|
||||
- Regular inventory users can view stock and enter counted quantities
|
||||
- Only privileged users can apply the adjustments to modify actual stock levels
|
||||
- **No Impact on Automatic Operations**: Stock moves from pickings, manufacturing orders, and other automatic operations work normally
|
||||
|
||||
## Use Case
|
||||
|
||||
Perfect for organizations that want to:
|
||||
- Allow warehouse staff to participate in inventory counts
|
||||
- Restrict who can actually modify stock levels
|
||||
- Maintain audit trails by limiting adjustment privileges
|
||||
- Implement proper inventory control procedures
|
||||
|
||||
## How It Works
|
||||
|
||||
### Technical Implementation
|
||||
|
||||
The module overrides the `write()` method on `stock.quant` to check:
|
||||
1. If the operation is in "inventory mode" (manual adjustment context)
|
||||
2. If the `quantity` field is being modified
|
||||
3. If the user has the required security group
|
||||
|
||||
The check uses Odoo's built-in `_is_inventory_mode()` method which returns `True` only when:
|
||||
- The `inventory_mode` context flag is set (manual adjustments)
|
||||
- The user has the `stock.group_stock_user` group
|
||||
|
||||
### User Experience
|
||||
|
||||
**Regular Inventory User:**
|
||||
- Can navigate to Inventory > Inventory Adjustments
|
||||
- Can view current stock quantities
|
||||
- Can enter counted quantities in the `inventory_quantity` field
|
||||
- **Cannot** click "Apply" to commit the changes
|
||||
- Receives clear error message when attempting to apply
|
||||
|
||||
**Privileged User (with Manual Adjustments group):**
|
||||
- Can do everything a regular user can do
|
||||
- **Can** click "Apply" to commit inventory adjustments
|
||||
- Can modify actual stock quantities
|
||||
|
||||
## Configuration
|
||||
|
||||
1. Install the module
|
||||
2. Go to Settings > Users & Companies > Users
|
||||
3. Edit a user who should be able to apply inventory adjustments
|
||||
4. Add them to the "Inventory: Manual Adjustments" group
|
||||
|
||||
## Testing
|
||||
|
||||
The module includes comprehensive test coverage:
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
odoo-bin -d your_database -i stock_inventory_adjustment_security --test-enable --stop-after-init
|
||||
|
||||
# Run specific test class
|
||||
odoo-bin -d your_database --test-tags stock_inventory_adjustment_security
|
||||
```
|
||||
|
||||
### Test Cases
|
||||
|
||||
- ✅ Regular users can set inventory_quantity (count)
|
||||
- ✅ Regular users cannot apply adjustments
|
||||
- ✅ Privileged users can apply adjustments
|
||||
- ✅ Automatic operations (pickings, etc.) are not affected
|
||||
- ✅ Inventory mode detection works correctly
|
||||
- ✅ Multiple quant adjustments work correctly
|
||||
- ✅ Non-inventory mode writes are not restricted
|
||||
|
||||
## Compatibility
|
||||
|
||||
- Odoo 18.0
|
||||
- Depends on: `stock`
|
||||
|
||||
## Translations
|
||||
|
||||
- English (en)
|
||||
- French (fr)
|
||||
|
||||
## License
|
||||
|
||||
LGPL-3
|
||||
|
||||
## Author
|
||||
|
||||
Bemade Inc.
|
||||
https://bemade.org
|
||||
1
stock_inventory_adjustment_security/__init__.py
Normal file
1
stock_inventory_adjustment_security/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import models
|
||||
30
stock_inventory_adjustment_security/__manifest__.py
Normal file
30
stock_inventory_adjustment_security/__manifest__.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"name": "Stock Inventory Adjustment Security",
|
||||
"author": "Bemade Inc.",
|
||||
"version": "18.0.1.0.0",
|
||||
"category": "Inventory/Inventory",
|
||||
"summary": "Restrict manual inventory adjustments to privileged users",
|
||||
"description": """
|
||||
Stock Inventory Adjustment Security
|
||||
====================================
|
||||
|
||||
This module adds an additional security layer for inventory adjustments:
|
||||
|
||||
* Creates a new security group "Inventory: Manual Adjustments"
|
||||
* Regular inventory users can view and count inventory (set inventory_quantity)
|
||||
* Only users in the privileged group can apply adjustments (modify actual quantity)
|
||||
* Automatic stock operations (pickings, manufacturing, etc.) are not affected
|
||||
|
||||
This allows inventory staff to participate in counts while restricting who can
|
||||
actually apply the adjustments and modify stock levels.
|
||||
""",
|
||||
"website": "https://www.bemade.org",
|
||||
"depends": ["stock"],
|
||||
"data": [
|
||||
"security/security.xml",
|
||||
],
|
||||
"installable": True,
|
||||
"auto_install": False,
|
||||
"license": "LGPL-3",
|
||||
"application": False,
|
||||
}
|
||||
44
stock_inventory_adjustment_security/i18n/fr.po
Normal file
44
stock_inventory_adjustment_security/i18n/fr.po
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * stock_inventory_adjustment_security
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 18.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-02 21:15:00+0000\n"
|
||||
"PO-Revision-Date: 2025-10-02 21:15:00+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"Language: fr\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#. module: stock_inventory_adjustment_security
|
||||
#: code:addons/stock_inventory_adjustment_security/models/stock_quant.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"You don't have permission to apply inventory adjustments. You can count "
|
||||
"inventory, but only authorized users can apply the changes. Please contact "
|
||||
"your inventory manager."
|
||||
msgstr ""
|
||||
"Vous n'avez pas la permission d'appliquer des ajustements d'inventaire. "
|
||||
"Vous pouvez compter l'inventaire, mais seuls les utilisateurs autorisés "
|
||||
"peuvent appliquer les modifications. Veuillez contacter votre gestionnaire "
|
||||
"d'inventaire."
|
||||
|
||||
#. module: stock_inventory_adjustment_security
|
||||
#: model:res.groups,name:stock_inventory_adjustment_security.group_inventory_manual_adjustments
|
||||
msgid "Manual Adjustments"
|
||||
msgstr "Ajustements manuels"
|
||||
|
||||
#. module: stock_inventory_adjustment_security
|
||||
#: model:res.groups,comment:stock_inventory_adjustment_security.group_inventory_manual_adjustments
|
||||
msgid ""
|
||||
"Users in this group can apply manual inventory adjustments.\n"
|
||||
"Regular inventory users can count inventory but cannot apply the adjustments."
|
||||
msgstr ""
|
||||
"Les utilisateurs de ce groupe peuvent appliquer des ajustements d'inventaire manuels.\n"
|
||||
"Les utilisateurs d'inventaire réguliers peuvent compter l'inventaire mais ne peuvent pas appliquer les ajustements."
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * stock_inventory_adjustment_security
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 18.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-02 21:15:00+0000\n"
|
||||
"PO-Revision-Date: 2025-10-02 21:15:00+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: stock_inventory_adjustment_security
|
||||
#: code:addons/stock_inventory_adjustment_security/models/stock_quant.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"You don't have permission to apply inventory adjustments. You can count "
|
||||
"inventory, but only authorized users can apply the changes. Please contact "
|
||||
"your inventory manager."
|
||||
msgstr ""
|
||||
|
||||
#. module: stock_inventory_adjustment_security
|
||||
#: model:res.groups,name:stock_inventory_adjustment_security.group_inventory_manual_adjustments
|
||||
msgid "Manual Adjustments"
|
||||
msgstr ""
|
||||
|
||||
#. module: stock_inventory_adjustment_security
|
||||
#: model:res.groups,comment:stock_inventory_adjustment_security.group_inventory_manual_adjustments
|
||||
msgid ""
|
||||
"Users in this group can apply manual inventory adjustments.\n"
|
||||
"Regular inventory users can count inventory but cannot apply the adjustments."
|
||||
msgstr ""
|
||||
1
stock_inventory_adjustment_security/models/__init__.py
Normal file
1
stock_inventory_adjustment_security/models/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import stock_quant
|
||||
61
stock_inventory_adjustment_security/models/stock_quant.py
Normal file
61
stock_inventory_adjustment_security/models/stock_quant.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models, _, api
|
||||
from odoo.exceptions import AccessError
|
||||
|
||||
|
||||
class StockQuant(models.Model):
|
||||
_inherit = "stock.quant"
|
||||
|
||||
def _check_inventory_adjustment_permission(self):
|
||||
"""
|
||||
Check if the current user has permission to make inventory adjustments.
|
||||
|
||||
Raises AccessError if the user lacks the required group.
|
||||
"""
|
||||
if not self.env.user.has_group(
|
||||
"stock_inventory_adjustment_security.group_inventory_manual_adjustments"
|
||||
):
|
||||
raise AccessError(
|
||||
_(
|
||||
"You don't have permission to apply inventory adjustments. "
|
||||
"You can count inventory, but only authorized users can apply the changes. "
|
||||
"Please contact your inventory manager."
|
||||
)
|
||||
)
|
||||
|
||||
def action_apply_inventory(self):
|
||||
"""
|
||||
Override action_apply_inventory to restrict who can apply adjustments.
|
||||
|
||||
This is the main entry point for applying manual inventory adjustments.
|
||||
Regular users can count, but only privileged users can apply.
|
||||
"""
|
||||
self._check_inventory_adjustment_permission()
|
||||
return super().action_apply_inventory()
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
"""
|
||||
Override create to restrict quant creation in inventory mode.
|
||||
|
||||
Regular users should not be able to create new quants in inventory mode
|
||||
as this is another way to manipulate inventory levels.
|
||||
"""
|
||||
if self._is_inventory_mode():
|
||||
self._check_inventory_adjustment_permission()
|
||||
|
||||
return super().create(vals_list)
|
||||
|
||||
def write(self, vals):
|
||||
"""
|
||||
Override write to restrict quantity changes in inventory mode.
|
||||
|
||||
This catches direct quantity modifications during inventory adjustments.
|
||||
Automatic operations (pickings, manufacturing, etc.) don't set
|
||||
inventory_mode context, so they are unaffected.
|
||||
"""
|
||||
if self._is_inventory_mode() and "quantity" in vals:
|
||||
self._check_inventory_adjustment_permission()
|
||||
|
||||
return super().write(vals)
|
||||
17
stock_inventory_adjustment_security/security/security.xml
Normal file
17
stock_inventory_adjustment_security/security/security.xml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="group_inventory_manual_adjustments" model="res.groups">
|
||||
<field name="name">Manual Adjustments</field>
|
||||
<field name="category_id" ref="base.module_category_inventory_inventory"/>
|
||||
<field name="implied_ids" eval="[(4, ref('stock.group_stock_user'))]"/>
|
||||
<field name="comment">
|
||||
Users in this group can apply manual inventory adjustments.
|
||||
Regular inventory users can count inventory but cannot apply the adjustments.
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Make stock manager imply manual adjustments -->
|
||||
<record id="stock.group_stock_manager" model="res.groups">
|
||||
<field name="implied_ids" eval="[(4, ref('group_inventory_manual_adjustments'))]"/>
|
||||
</record>
|
||||
</odoo>
|
||||
1
stock_inventory_adjustment_security/tests/__init__.py
Normal file
1
stock_inventory_adjustment_security/tests/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import test_inventory_adjustment_security
|
||||
|
|
@ -0,0 +1,326 @@
|
|||
from odoo.tests.common import TransactionCase
|
||||
from odoo.exceptions import AccessError
|
||||
|
||||
|
||||
class TestInventoryAdjustmentSecurity(TransactionCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
# Create test product
|
||||
cls.product = cls.env["product.product"].create(
|
||||
{
|
||||
"name": "Test Product",
|
||||
"type": "consu",
|
||||
"is_storable": True,
|
||||
}
|
||||
)
|
||||
|
||||
# Get stock location
|
||||
cls.stock_location = cls.env.ref("stock.warehouse0").lot_stock_id
|
||||
|
||||
# Create initial quant with some quantity
|
||||
cls.quant = (
|
||||
cls.env["stock.quant"]
|
||||
.sudo()
|
||||
.create(
|
||||
{
|
||||
"product_id": cls.product.id,
|
||||
"location_id": cls.stock_location.id,
|
||||
"quantity": 100.0,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
# Create test users
|
||||
# Regular inventory user (can count but not apply)
|
||||
cls.inventory_user = cls.env["res.users"].create(
|
||||
{
|
||||
"name": "Inventory Counter",
|
||||
"login": "inventory_counter",
|
||||
"email": "counter@test.com",
|
||||
"groups_id": [(6, 0, [cls.env.ref("stock.group_stock_user").id])],
|
||||
}
|
||||
)
|
||||
|
||||
# Privileged user (can count and apply)
|
||||
cls.privileged_user = cls.env["res.users"].create(
|
||||
{
|
||||
"name": "Inventory Manager",
|
||||
"login": "inventory_manager",
|
||||
"email": "manager@test.com",
|
||||
"groups_id": [
|
||||
(
|
||||
6,
|
||||
0,
|
||||
[
|
||||
cls.env.ref("stock.group_stock_user").id,
|
||||
cls.env.ref(
|
||||
"stock_inventory_adjustment_security.group_inventory_manual_adjustments"
|
||||
).id,
|
||||
],
|
||||
)
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
def test_regular_user_can_set_inventory_quantity(self):
|
||||
"""Test that regular users can set inventory_quantity (count)"""
|
||||
quant = self.quant.with_user(self.inventory_user).with_context(
|
||||
inventory_mode=True
|
||||
)
|
||||
|
||||
# Should be able to set inventory_quantity
|
||||
quant.write({"inventory_quantity": 90.0})
|
||||
self.assertEqual(quant.inventory_quantity, 90.0)
|
||||
self.assertEqual(quant.inventory_diff_quantity, -10.0)
|
||||
|
||||
def test_regular_user_cannot_apply_adjustment(self):
|
||||
"""Test that regular users cannot apply adjustments (modify quantity)"""
|
||||
quant = self.quant.with_user(self.inventory_user).with_context(inventory_mode=True)
|
||||
|
||||
# Set inventory quantity first
|
||||
quant.write({"inventory_quantity": 90.0})
|
||||
|
||||
# Trying to apply (which writes to quantity field) should fail
|
||||
with self.assertRaises(AccessError):
|
||||
quant.write({"quantity": 90.0})
|
||||
|
||||
def test_regular_user_cannot_create_quants_in_inventory_mode(self):
|
||||
"""Test that regular users cannot create new quants in inventory mode"""
|
||||
# Create a new product
|
||||
new_product = self.env["product.product"].create(
|
||||
{
|
||||
"name": "New Product",
|
||||
"type": "consu",
|
||||
"is_storable": True,
|
||||
}
|
||||
)
|
||||
|
||||
# Trying to create a quant in inventory mode should fail
|
||||
with self.assertRaises(AccessError):
|
||||
self.env["stock.quant"].with_user(self.inventory_user).with_context(
|
||||
inventory_mode=True
|
||||
).create(
|
||||
{
|
||||
"product_id": new_product.id,
|
||||
"location_id": self.stock_location.id,
|
||||
"quantity": 50.0,
|
||||
}
|
||||
)
|
||||
|
||||
def test_regular_user_cannot_call_apply_inventory(self):
|
||||
"""Test that regular users cannot call action_apply_inventory"""
|
||||
quant = self.quant.with_user(self.inventory_user).with_context(
|
||||
inventory_mode=True
|
||||
)
|
||||
|
||||
# Set inventory quantity
|
||||
quant.write({"inventory_quantity": 90.0})
|
||||
|
||||
# Trying to apply inventory should fail
|
||||
with self.assertRaises(AccessError):
|
||||
quant.action_apply_inventory()
|
||||
|
||||
def test_privileged_user_can_apply_adjustment(self):
|
||||
"""Test that privileged users can apply adjustments"""
|
||||
quant = self.quant.with_user(self.privileged_user).with_context(
|
||||
inventory_mode=True
|
||||
)
|
||||
|
||||
# Set inventory quantity
|
||||
quant.write({"inventory_quantity": 90.0})
|
||||
|
||||
# Should be able to apply the adjustment
|
||||
quant.action_apply_inventory()
|
||||
|
||||
# Quantity should be updated
|
||||
self.assertEqual(quant.quantity, 90.0)
|
||||
self.assertEqual(quant.inventory_quantity_set, False)
|
||||
|
||||
def test_privileged_user_can_create_quants_in_inventory_mode(self):
|
||||
"""Test that privileged users can create new quants in inventory mode"""
|
||||
# Create a new product
|
||||
new_product = self.env["product.product"].create(
|
||||
{
|
||||
"name": "Privileged Product",
|
||||
"type": "consu",
|
||||
"is_storable": True,
|
||||
}
|
||||
)
|
||||
|
||||
# Privileged user should be able to create a quant in inventory mode
|
||||
quant = self.env["stock.quant"].with_user(self.privileged_user).with_context(
|
||||
inventory_mode=True
|
||||
).create(
|
||||
{
|
||||
"product_id": new_product.id,
|
||||
"location_id": self.stock_location.id,
|
||||
"quantity": 50.0,
|
||||
}
|
||||
)
|
||||
|
||||
self.assertEqual(quant.quantity, 50.0)
|
||||
self.assertEqual(quant.product_id, new_product)
|
||||
|
||||
def test_automatic_operations_not_affected(self):
|
||||
"""Test that automatic stock operations work normally"""
|
||||
# Create a stock move (simulating a picking or manufacturing operation)
|
||||
# These operations don't set inventory_mode context
|
||||
move = self.env["stock.move"].create(
|
||||
{
|
||||
"name": "Test Move",
|
||||
"product_id": self.product.id,
|
||||
"product_uom_qty": 10.0,
|
||||
"product_uom": self.product.uom_id.id,
|
||||
"location_id": self.env.ref("stock.stock_location_suppliers").id,
|
||||
"location_dest_id": self.stock_location.id,
|
||||
}
|
||||
)
|
||||
|
||||
move._action_confirm()
|
||||
move._action_assign()
|
||||
# Set quantity on the move itself
|
||||
move.quantity = 10.0
|
||||
move.picked = True
|
||||
move._action_done()
|
||||
|
||||
# Quant quantity should be updated automatically
|
||||
# Find the quant for this product in stock location
|
||||
quant = self.env["stock.quant"].search(
|
||||
[
|
||||
("product_id", "=", self.product.id),
|
||||
("location_id", "=", self.stock_location.id),
|
||||
]
|
||||
)
|
||||
self.assertEqual(quant.quantity, 110.0)
|
||||
|
||||
def test_regular_user_can_view_quantities(self):
|
||||
"""Test that regular users can view all quantity fields"""
|
||||
quant = self.quant.with_user(self.inventory_user)
|
||||
|
||||
# Should be able to read all fields
|
||||
self.assertEqual(quant.quantity, 100.0)
|
||||
self.assertEqual(quant.inventory_quantity, 0.0)
|
||||
|
||||
# Should be able to read in inventory mode too
|
||||
quant_inv_mode = quant.with_context(inventory_mode=True)
|
||||
self.assertEqual(quant_inv_mode.quantity, 100.0)
|
||||
|
||||
def test_non_inventory_mode_writes_allowed(self):
|
||||
"""Test that quantity writes outside inventory mode are not restricted"""
|
||||
# Even regular users can write quantity when not in inventory_mode
|
||||
# (though they typically wouldn't have access to do this via UI)
|
||||
quant = self.quant.sudo()
|
||||
|
||||
# Without inventory_mode context, write should work
|
||||
quant.write({"quantity": 95.0})
|
||||
|
||||
self.assertEqual(quant.quantity, 95.0)
|
||||
|
||||
def test_inventory_mode_detection(self):
|
||||
"""Test that _is_inventory_mode() works correctly"""
|
||||
quant = self.quant.with_user(self.inventory_user)
|
||||
|
||||
# Without inventory_mode context
|
||||
self.assertFalse(quant._is_inventory_mode())
|
||||
|
||||
# With inventory_mode context
|
||||
quant_inv = quant.with_context(inventory_mode=True)
|
||||
self.assertTrue(quant_inv._is_inventory_mode())
|
||||
|
||||
# User without stock_user group shouldn't trigger inventory mode
|
||||
basic_user = self.env["res.users"].create(
|
||||
{
|
||||
"name": "Basic User",
|
||||
"login": "basic_user",
|
||||
"email": "basic@test.com",
|
||||
"groups_id": [(6, 0, [self.env.ref("base.group_user").id])],
|
||||
}
|
||||
)
|
||||
quant_basic = self.quant.with_user(basic_user).with_context(inventory_mode=True)
|
||||
self.assertFalse(quant_basic._is_inventory_mode())
|
||||
|
||||
def test_multiple_quants_adjustment(self):
|
||||
"""Test applying adjustments to multiple quants at once"""
|
||||
# Create another product to avoid quant merging
|
||||
product2 = self.env["product.product"].create(
|
||||
{
|
||||
"name": "Test Product 2",
|
||||
"type": "consu",
|
||||
"is_storable": True,
|
||||
}
|
||||
)
|
||||
|
||||
# Create another quant with different product
|
||||
quant2 = (
|
||||
self.env["stock.quant"]
|
||||
.sudo()
|
||||
.create(
|
||||
{
|
||||
"product_id": product2.id,
|
||||
"location_id": self.stock_location.id,
|
||||
"quantity": 50.0,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
# Set inventory quantities on both quants
|
||||
self.quant.with_user(self.privileged_user).with_context(
|
||||
inventory_mode=True
|
||||
).write({"inventory_quantity": 80.0})
|
||||
quant2.with_user(self.privileged_user).with_context(inventory_mode=True).write(
|
||||
{"inventory_quantity": 40.0}
|
||||
)
|
||||
|
||||
# Apply all at once
|
||||
quants = (
|
||||
(self.quant | quant2)
|
||||
.with_user(self.privileged_user)
|
||||
.with_context(inventory_mode=True)
|
||||
)
|
||||
quants.action_apply_inventory()
|
||||
|
||||
self.assertEqual(self.quant.quantity, 80.0)
|
||||
self.assertEqual(quant2.quantity, 40.0)
|
||||
|
||||
def test_regular_user_multiple_quants_blocked(self):
|
||||
"""Test that regular users cannot apply multiple quants"""
|
||||
# Create another product to avoid quant merging
|
||||
product2 = self.env["product.product"].create(
|
||||
{
|
||||
"name": "Test Product 3",
|
||||
"type": "consu",
|
||||
"is_storable": True,
|
||||
}
|
||||
)
|
||||
|
||||
quant2 = (
|
||||
self.env["stock.quant"]
|
||||
.sudo()
|
||||
.create(
|
||||
{
|
||||
"product_id": product2.id,
|
||||
"location_id": self.stock_location.id,
|
||||
"quantity": 50.0,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
# Set inventory quantities
|
||||
self.quant.with_user(self.inventory_user).with_context(
|
||||
inventory_mode=True
|
||||
).write({"inventory_quantity": 80.0})
|
||||
quant2.with_user(self.inventory_user).with_context(inventory_mode=True).write(
|
||||
{"inventory_quantity": 40.0}
|
||||
)
|
||||
|
||||
# Trying to apply should fail
|
||||
quants = (
|
||||
(self.quant | quant2)
|
||||
.with_user(self.inventory_user)
|
||||
.with_context(inventory_mode=True)
|
||||
)
|
||||
with self.assertRaises(AccessError):
|
||||
quants.action_apply_inventory()
|
||||
2
test_caldav_sync_appointments/__init__.py
Normal file
2
test_caldav_sync_appointments/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Test module for CalDAV sync appointments integration
|
||||
28
test_caldav_sync_appointments/__manifest__.py
Normal file
28
test_caldav_sync_appointments/__manifest__.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': 'Test CalDAV Sync Appointments Integration',
|
||||
'version': '18.0.1.0.0',
|
||||
'category': 'Hidden/Tests',
|
||||
'summary': 'Test module for CalDAV sync integration with appointments',
|
||||
'description': """
|
||||
Test CalDAV Sync Appointments Integration
|
||||
=========================================
|
||||
|
||||
This module contains tests for the integration between CalDAV sync and the
|
||||
Appointments app. It verifies that appointment events are properly handled
|
||||
during CalDAV synchronization operations.
|
||||
|
||||
This is a test-only module and should not be installed in production.
|
||||
""",
|
||||
'author': 'Bemade Inc.',
|
||||
'website': 'https://www.bemade.org',
|
||||
'depends': [
|
||||
'caldav_sync',
|
||||
'appointment',
|
||||
],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
'application': False,
|
||||
# Mark as test module
|
||||
'post_init_hook': None,
|
||||
}
|
||||
1
test_caldav_sync_appointments/tests/__init__.py
Normal file
1
test_caldav_sync_appointments/tests/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import test_appointment_caldav_integration
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
"""Test CalDAV sync integration with appointment events."""
|
||||
|
||||
import logging
|
||||
from unittest.mock import patch, MagicMock
|
||||
from datetime import datetime, timedelta
|
||||
from contextlib import contextmanager
|
||||
|
||||
from odoo.tests.common import TransactionCase, tagged
|
||||
from odoo import Command
|
||||
from odoo.addons.caldav_sync.tests.common import CaldavTestCommon
|
||||
from odoo.tools import mute_logger
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _patch_caldav_for_appointments(user):
|
||||
"""Patch CalDAV operations for appointment testing, capturing real iCalendar events."""
|
||||
with mute_logger("odoo.addons.caldav_sync.models.res_users"), patch(
|
||||
"caldav.DAVClient"
|
||||
) as MockDAVClient:
|
||||
mock_client = MockDAVClient.return_value
|
||||
mock_calendar = MagicMock()
|
||||
mock_client.calendar.return_value = mock_calendar
|
||||
|
||||
# Storage for created CalDAV events and global delete tracking
|
||||
created_events = {} # uid -> caldav_event
|
||||
global_delete_calls = [] # Track all delete calls
|
||||
|
||||
def save_event_side_effect(**ical_data):
|
||||
"""Let the real CalDAV event creation happen, then capture and store it."""
|
||||
# Create a mock CalDAV event that we can track delete() calls on
|
||||
mock_caldav_event = MagicMock()
|
||||
|
||||
# Set up the vobject_instance structure that CalDAV sync expects
|
||||
uid = ical_data.get("uid", f"test-uid-{len(created_events)}")
|
||||
mock_caldav_event.vobject_instance.vevent.uid.value = uid
|
||||
|
||||
# Ensure delete method is properly trackable and records calls globally
|
||||
def delete_side_effect():
|
||||
global_delete_calls.append(uid)
|
||||
|
||||
mock_caldav_event.delete = MagicMock(side_effect=delete_side_effect)
|
||||
|
||||
# Set up icalendar_component structure using the real iCalendar data
|
||||
mock_ical_component = MagicMock()
|
||||
|
||||
def ical_component_get(key):
|
||||
if key == "dtstart":
|
||||
mock_dtstart = MagicMock()
|
||||
dtstart_value = ical_data.get("dtstart")
|
||||
|
||||
# Handle vDatetime objects from iCalendar library
|
||||
if hasattr(dtstart_value, "dt"):
|
||||
# It's already a vDatetime, extract the actual datetime
|
||||
actual_dt = dtstart_value.dt
|
||||
else:
|
||||
# It's a regular datetime
|
||||
actual_dt = dtstart_value
|
||||
|
||||
# Create a mock that has tzinfo like a regular datetime
|
||||
mock_dt = MagicMock()
|
||||
mock_dt.tzinfo = getattr(actual_dt, "tzinfo", None)
|
||||
mock_dt.__eq__ = lambda self, other: actual_dt == other
|
||||
|
||||
mock_dtstart.dt = mock_dt
|
||||
return mock_dtstart
|
||||
return MagicMock()
|
||||
|
||||
mock_ical_component.get.side_effect = ical_component_get
|
||||
mock_caldav_event.icalendar_component = mock_ical_component
|
||||
|
||||
# Store the event by UID for later retrieval
|
||||
created_events[uid] = mock_caldav_event
|
||||
|
||||
return mock_caldav_event
|
||||
|
||||
def event_by_uid_side_effect(uid):
|
||||
"""Return the previously created CalDAV event by UID."""
|
||||
if uid in created_events:
|
||||
# Always return the same mock object for the same UID
|
||||
return created_events[uid]
|
||||
# Raise NotFoundError if event doesn't exist (like real CalDAV)
|
||||
from caldav.lib.error import NotFoundError
|
||||
|
||||
raise NotFoundError("Event not found")
|
||||
|
||||
# Set up the mock methods
|
||||
mock_calendar.events.return_value = []
|
||||
mock_calendar.save_event.side_effect = save_event_side_effect
|
||||
mock_calendar.event_by_uid.side_effect = event_by_uid_side_effect
|
||||
|
||||
# Enable CalDAV for the user
|
||||
user.write({"is_caldav_enabled": True})
|
||||
|
||||
# Return both the mock calendar and the global delete tracking
|
||||
mock_calendar.global_delete_calls = global_delete_calls
|
||||
yield mock_calendar
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestAppointmentCalDAVIntegration(TransactionCase, CaldavTestCommon):
|
||||
"""Test CalDAV sync behavior with appointment events."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
# Create test user with CalDAV enabled using the common method
|
||||
cls.test_user = cls._generate_user(
|
||||
"test_caldav_user",
|
||||
caldav_username="testuser",
|
||||
caldav_password="testpass",
|
||||
caldav_url="https://test.caldav.server/calendar/testuser",
|
||||
)
|
||||
|
||||
# Create test partner
|
||||
cls.test_partner = cls.env["res.partner"].create(
|
||||
{
|
||||
"name": "Test Partner",
|
||||
"email": "partner@example.com",
|
||||
}
|
||||
)
|
||||
|
||||
# Create appointment type
|
||||
cls.appointment_type = cls.env["appointment.type"].create(
|
||||
{
|
||||
"category": "custom",
|
||||
"appointment_duration": 1.0,
|
||||
"staff_user_ids": [Command.link(cls.test_user.id)],
|
||||
}
|
||||
)
|
||||
|
||||
def test_appointment_booking_syncs_to_caldav(self):
|
||||
"""Test that appointments created through the booking interface sync to CalDAV.
|
||||
|
||||
BUG: When a public user books an appointment through the booking interface,
|
||||
the event is created in Odoo but NOT synced to the CalDAV server. This causes
|
||||
the event to be deleted on the next polling round since CalDAV sync thinks it
|
||||
was deleted from the server.
|
||||
"""
|
||||
|
||||
with _patch_caldav_for_appointments(self.test_user) as mock_calendar:
|
||||
# Simulate the appointment booking flow - appointments are created with
|
||||
# the staff user but initiated by a public/portal user
|
||||
# The key is that we need to simulate how the appointment controller creates events
|
||||
|
||||
# Create appointment event as it would be created during booking
|
||||
appointment_event = (
|
||||
self.env["calendar.event"]
|
||||
.with_user(self.test_user)
|
||||
.create(
|
||||
{
|
||||
"name": f"{self.appointment_type.name} with {self.test_user.name}",
|
||||
"start": datetime.now() + timedelta(days=1),
|
||||
"stop": datetime.now() + timedelta(days=1, hours=1),
|
||||
"user_id": self.test_user.id,
|
||||
"partner_ids": [(4, self.test_partner.id)],
|
||||
"appointment_type_id": self.appointment_type.id,
|
||||
"appointment_status": "booked",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
# Debug output
|
||||
print(f"\nDEBUG Appointment Booking:")
|
||||
print(f" save_event called: {mock_calendar.save_event.called}")
|
||||
print(f" save_event call_count: {mock_calendar.save_event.call_count}")
|
||||
print(f" caldav_uid: {appointment_event.caldav_uid}")
|
||||
print(f" user_id: {appointment_event.user_id.name}")
|
||||
print(f" is_caldav_enabled: {appointment_event._is_caldav_enabled()}")
|
||||
|
||||
# THE BUG: Appointment events should be synced to CalDAV on creation
|
||||
# This will FAIL if the bug exists
|
||||
self.assertTrue(
|
||||
mock_calendar.save_event.called,
|
||||
"BUG: Appointment event was NOT synced to CalDAV server on creation! "
|
||||
"This will cause it to be deleted on next polling round.",
|
||||
)
|
||||
self.assertTrue(
|
||||
appointment_event.caldav_uid,
|
||||
"BUG: Appointment event has no caldav_uid! Event will be deleted on next sync.",
|
||||
)
|
||||
Loading…
Reference in a new issue