Compare commits

...

5 commits

22 changed files with 840 additions and 461 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View 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

View file

@ -0,0 +1 @@
from . import models

View 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,
}

View 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."

View file

@ -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 ""

View file

@ -0,0 +1 @@
from . import stock_quant

View 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)

View 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>

View file

@ -0,0 +1 @@
from . import test_inventory_adjustment_security

View file

@ -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()

View file

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
# Test module for CalDAV sync appointments integration

View 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,
}

View file

@ -0,0 +1 @@
from . import test_appointment_caldav_integration

View file

@ -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.",
)