Compare commits
103 commits
Feature/AI
...
18.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
278540fbaf | ||
|
|
a080586ef4 | ||
|
|
3f5c5cc038 | ||
|
|
00a940672a | ||
|
|
ece737826c | ||
|
|
800214b19c | ||
|
|
9e7662ec7e | ||
|
|
b779529d91 | ||
|
|
8f252372e4 | ||
|
|
cbd9f2faab | ||
|
|
dffb2755db | ||
|
|
3cc5889ea4 | ||
|
|
f6cdd94f5f | ||
|
|
21cc63df0b | ||
|
|
f830cb676f | ||
|
|
73c74412e0 | ||
|
|
59fa4703ab | ||
|
|
7b8255abe2 | ||
|
|
6accef74e2 | ||
|
|
2d748d7b4e | ||
|
|
e0f55f4458 | ||
|
|
8e69382d84 | ||
|
|
4142187feb | ||
|
|
d428d673bc | ||
|
|
1a69864176 | ||
|
|
d69c2425ee | ||
|
|
e1a64337ff | ||
|
|
289e8f3731 | ||
|
|
7f62a24819 | ||
|
|
2d0577d7e3 | ||
|
|
c78b926565 | ||
|
|
ba9f1e0c56 | ||
|
|
5ec51c3554 | ||
|
|
c19f2e29e6 | ||
|
|
8662ad3ffd | ||
|
|
975870333a | ||
|
|
ca493863cb | ||
|
|
5793351bbe | ||
|
|
c7d0a1be2b | ||
|
|
bb8c1f2f35 | ||
|
|
06584b381a | ||
|
|
6ea4b324b3 | ||
|
|
15f96ec795 | ||
|
|
917e04d018 | ||
|
|
1fe33c306a | ||
|
|
c43a615dd4 | ||
|
|
ce69990df6 | ||
|
|
8424815d53 | ||
|
|
fc8248ff2e | ||
|
|
06b9c6d201 | ||
|
|
a47e829935 | ||
|
|
6872e5fac2 | ||
|
|
65b301b2e8 | ||
|
|
583e25e092 | ||
|
|
e5e90d6c7e | ||
|
|
736f7011a7 | ||
|
|
6c096430d5 | ||
|
|
0c156f089b | ||
|
|
03ca1649c9 | ||
|
|
f5e1751da7 | ||
|
|
8ca4ef5af3 | ||
|
|
c03886e812 | ||
|
|
29a087b207 | ||
|
|
37ac446c1e | ||
|
|
a48450849c | ||
|
|
580e61d840 | ||
|
|
2d052d630b | ||
|
|
fd43de7443 | ||
|
|
aea07cb5a5 | ||
|
|
3ce8d051bc | ||
|
|
78589bbcf3 | ||
|
|
e779b4da94 | ||
|
|
805c694e21 | ||
|
|
d2286d136e | ||
|
|
0c0d047f2e | ||
|
|
b755b80150 | ||
|
|
721ab5776c | ||
|
|
fd1751b058 | ||
|
|
e0c4fcba6f | ||
|
|
7f3b330ca9 | ||
|
|
c135c3eef7 | ||
|
|
5abdebb3a1 | ||
|
|
cdef6664d2 | ||
|
|
3ce3b2a1de | ||
|
|
8e98592306 | ||
|
|
53a027c87c | ||
|
|
5137d23562 | ||
|
|
02eff1880f | ||
|
|
14fa714fe8 | ||
|
|
f5911f0767 | ||
|
|
fae0d98637 | ||
|
|
82266644ca | ||
|
|
a6742c16b0 | ||
|
|
8e53a0863f | ||
|
|
85a2a3fafe | ||
|
|
a8aa7e71c2 | ||
|
|
90dc45245e | ||
|
|
6cfb9551b0 | ||
|
|
eff17d674d | ||
|
|
c0834eb327 | ||
|
|
8c9d60c550 | ||
|
|
e81efd7fdd | ||
|
|
ca57ec8f16 |
542 changed files with 32280 additions and 12277 deletions
|
|
@ -37,12 +37,12 @@ repos:
|
|||
language: fail
|
||||
files: '[a-zA-Z0-9_]*/i18n/en\.po$'
|
||||
- repo: https://github.com/OCA/odoo-pre-commit-hooks
|
||||
rev: v0.0.25
|
||||
rev: v0.1.6
|
||||
hooks:
|
||||
- id: oca-checks-odoo-module
|
||||
- id: oca-checks-po
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v2.7.1
|
||||
rev: v4.0.0-alpha.8
|
||||
hooks:
|
||||
- id: prettier
|
||||
name: prettier (with plugin-xml)
|
||||
|
|
@ -53,7 +53,7 @@ repos:
|
|||
- --plugin=@prettier/plugin-xml
|
||||
files: \.(css|htm|html|js|json|jsx|less|md|scss|toml|ts|xml|yaml|yml)$
|
||||
- repo: https://github.com/pre-commit/mirrors-eslint
|
||||
rev: v8.24.0
|
||||
rev: v9.33.0
|
||||
hooks:
|
||||
- id: eslint
|
||||
verbose: true
|
||||
|
|
@ -61,7 +61,7 @@ repos:
|
|||
- --color
|
||||
- --fix
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.3.0
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
# exclude autogenerated files
|
||||
|
|
@ -83,7 +83,7 @@ repos:
|
|||
- id: mixed-line-ending
|
||||
args: ["--fix=lf"]
|
||||
- repo: https://github.com/OCA/pylint-odoo
|
||||
rev: v9.1.2
|
||||
rev: v9.3.14
|
||||
hooks:
|
||||
- id: pylint_odoo
|
||||
name: pylint with optional checks
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "Account Credit Hold",
|
||||
"version": "17.0.1.1.1",
|
||||
"version": "18.0.1.1.1",
|
||||
"summary": "Allows setting clients on credit hold, blocking the ability confirm a new sales order.",
|
||||
"category": "Accounting/Accounting",
|
||||
"author": "Bemade Inc.",
|
||||
|
|
|
|||
|
|
@ -1,246 +0,0 @@
|
|||
# Migration vers Odoo 18.0 - Module account_credit_hold
|
||||
|
||||
## Fonctionnalités
|
||||
- Ajoute un champ "Place on Credit Hold" sur les lignes de suivi de compte (account_followup.followup.line)
|
||||
- Ajoute des champs et fonctionnalités sur les partenaires:
|
||||
- postpone_hold_until: Date de report du blocage
|
||||
- hold_bg: Champ technique pour le statut de blocage
|
||||
- on_hold: État calculé du blocage de crédit
|
||||
- Bloque la confirmation des commandes de vente si le client est en blocage de crédit
|
||||
- Ajoute des indicateurs visuels (ruban rouge) sur:
|
||||
- Commandes de vente
|
||||
- Fiches partenaires
|
||||
- Transferts de stock
|
||||
- Ajoute des boutons pour mettre/lever le blocage de crédit dans la vue de suivi des comptes
|
||||
|
||||
## Analyse pour la Migration
|
||||
|
||||
### Dépendances
|
||||
- sale
|
||||
- account_followup
|
||||
- stock
|
||||
|
||||
### Changements Techniques Requis
|
||||
1. Mettre à jour la version dans __manifest__.py vers 18.0
|
||||
2. Vérifier la compatibilité des vues XML avec Odoo 18.0
|
||||
3. Vérifier si des changements dans l'API account_followup en 18.0
|
||||
|
||||
### Points d'Attention
|
||||
1. Le module utilise l'héritage de vues et de modèles standard d'Odoo:
|
||||
- account_followup.followup.line
|
||||
- res.partner
|
||||
- sale.order
|
||||
- stock.picking
|
||||
- account.followup.report
|
||||
|
||||
2. Fonctionnalités critiques à tester après migration:
|
||||
- Calcul automatique du statut on_hold
|
||||
- Blocage de la confirmation des commandes
|
||||
- Nettoyage automatique des reports de blocage expirés (@api.autovacuum)
|
||||
- Affichage correct des rubans d'avertissement
|
||||
- Propagation du statut hold aux contacts liés (commercial_partner_id)
|
||||
|
||||
3. Implémentation Technique:
|
||||
- Utilisation de champs computed avec store=True et compute_sudo=True
|
||||
- Mécanisme de nettoyage automatique via @api.autovacuum
|
||||
- Héritage de _execute_followup_partner pour automatisation du hold
|
||||
- Messages de chatter automatiques lors des changements de statut
|
||||
|
||||
4. Points Spécifiques aux Vues:
|
||||
- Utilisation du widget web_ribbon pour les indicateurs visuels
|
||||
- Boutons conditionnels dans la vue de suivi des comptes
|
||||
- Champs invisibles pour la logique d'affichage (hold_bg, on_hold)
|
||||
- Groupes de sécurité sur le champ postpone_hold_until
|
||||
|
||||
## Questions et Considérations
|
||||
|
||||
1. Vérifier si Odoo 18.0 n'a pas introduit des fonctionnalités natives similaires dans account_followup:
|
||||
- Système de blocage automatique des clients
|
||||
- Gestion des périodes de grâce
|
||||
- Indicateurs visuels de blocage
|
||||
|
||||
2. Points à valider:
|
||||
- La structure des vues héritées est-elle identique en 18.0?
|
||||
- Les champs related et computed fonctionnent-ils de la même manière?
|
||||
- Le système de suivi des comptes (account_followup) a-t-il évolué?
|
||||
- Le décorateur @api.autovacuum est-il toujours supporté?
|
||||
- Le widget web_ribbon utilise-t-il toujours la même API?
|
||||
|
||||
3. Considérations d'Architecture:
|
||||
- Le mécanisme de propagation du statut hold via commercial_partner_id est-il optimal?
|
||||
- Possibilité de simplifier la logique de calcul du statut hold?
|
||||
- Pertinence de stocker le champ hold_bg vs calcul à la demande
|
||||
|
||||
4. Alternatives Potentielles:
|
||||
- Utiliser le système de credit limit natif d'Odoo avec des règles personnalisées?
|
||||
- Intégrer avec le système de blocage des partenaires d'Odoo?
|
||||
- Utiliser les étapes de facturation (invoice_status) plutôt qu'un champ séparé?
|
||||
|
||||
## Alternatives Natives Odoo 18.0
|
||||
|
||||
### Système de Crédit Natif
|
||||
1. Odoo 18.0 inclut des fonctionnalités natives de gestion de crédit:
|
||||
- Champ `credit_limit` sur res.partner
|
||||
- Configuration du blocage au niveau de la société
|
||||
- Règles de blocage basées sur:
|
||||
- Montant de crédit maximum
|
||||
- Factures échues
|
||||
- Âge des factures
|
||||
|
||||
2. Possibilités d'utilisation des fonctionnalités natives:
|
||||
- Utiliser `credit_limit` au lieu de `on_hold`
|
||||
- Configurer les règles de blocage dans la configuration de la comptabilité
|
||||
- Utiliser les notifications natives de dépassement de crédit
|
||||
|
||||
### Améliorations Possibles
|
||||
1. Intégration avec le système natif:
|
||||
- Synchroniser notre `on_hold` avec le système natif de blocage
|
||||
- Utiliser les API natives de vérification de crédit
|
||||
- Conserver uniquement les fonctionnalités non disponibles nativement
|
||||
|
||||
2. Simplification du code:
|
||||
- Remplacer les champs custom par des champs natifs quand possible
|
||||
- Utiliser le système d'alertes natif pour les rubans
|
||||
- Intégrer avec le système de workflow natif
|
||||
|
||||
## Recommandations pour la Migration
|
||||
|
||||
### Approche "Vanilla First"
|
||||
1. Évaluer chaque fonctionnalité custom:
|
||||
- Est-elle disponible nativement dans Odoo 18.0?
|
||||
- Peut-elle être remplacée par une configuration native?
|
||||
- Le besoin business existe-t-il toujours?
|
||||
|
||||
2. Prioriser l'utilisation des fonctionnalités natives:
|
||||
- Système de crédit natif
|
||||
- Système de workflow natif
|
||||
- API de notification standard
|
||||
- Widgets standards de l'interface
|
||||
|
||||
### Modifications Techniques Recommandées
|
||||
1. Remplacer les attributs obsolètes:
|
||||
- Supprimer les `attrs` dans les vues (Odoo 16.0+)
|
||||
- Utiliser `list` au lieu de `tree` (Odoo 17.0+)
|
||||
- Adapter les widgets aux nouvelles conventions
|
||||
|
||||
2. Optimisation des performances:
|
||||
- Utiliser les indexes de base de données appropriés
|
||||
- Optimiser les recherches et calculs
|
||||
- Implémenter le lazy loading quand possible
|
||||
|
||||
### Plan de Test Approfondi
|
||||
1. Tests fonctionnels:
|
||||
- Validation du comportement avec le système natif
|
||||
- Tests de régression sur les fonctionnalités custom
|
||||
- Vérification des performances
|
||||
|
||||
2. Tests d'intégration:
|
||||
- Interaction avec le workflow de vente
|
||||
- Synchronisation avec la comptabilité
|
||||
- Comportement avec les autres modules
|
||||
|
||||
## État de la Migration
|
||||
⚪ En analyse préliminaire
|
||||
|
||||
## Plan de Migration
|
||||
|
||||
### Étape 1: Analyse des Changements Odoo 18.0
|
||||
- [ ] Examiner les changements dans account_followup
|
||||
- [ ] Vérifier les nouvelles fonctionnalités de gestion de crédit
|
||||
- [ ] Analyser les modifications des vues héritées
|
||||
|
||||
### Étape 2: Adaptation Technique
|
||||
- [ ] Mise à jour du manifeste
|
||||
- [ ] Vérification de la compatibilité des décorateurs
|
||||
- [ ] Adaptation des vues XML si nécessaire
|
||||
- [ ] Test des champs computed et related
|
||||
|
||||
### Étape 3: Tests Fonctionnels
|
||||
- [ ] Validation du mécanisme de hold
|
||||
- [ ] Test de la propagation aux contacts
|
||||
- [ ] Vérification des nettoyages automatiques
|
||||
- [ ] Test des indicateurs visuels
|
||||
|
||||
### Étape 4: Optimisation
|
||||
- [ ] Évaluation des alternatives natives
|
||||
- [ ] Simplification potentielle du code
|
||||
- [ ] Amélioration des performances
|
||||
|
||||
## Notes de Version
|
||||
- Version originale: 17.0.1.1.1
|
||||
- Dernière analyse: 26/01/2025
|
||||
|
||||
## Fonctionnalités Natives dans Odoo 18.0
|
||||
|
||||
Odoo 18.0 inclut nativement plusieurs fonctionnalités de gestion du crédit :
|
||||
|
||||
1. **Gestion des Limites de Crédit**
|
||||
- Champ `credit_limit` sur les partenaires
|
||||
- Champ `use_partner_credit_limit` pour activer/désactiver par partenaire
|
||||
- Configuration globale `account_use_credit_limit` au niveau de la société
|
||||
- Champ `credit` pour le total des créances
|
||||
- Champ `trust` pour le niveau de confiance du débiteur
|
||||
|
||||
2. **Visibilité et Contrôle**
|
||||
- Champ `show_credit_limit` basé sur la configuration de la société
|
||||
- Groupes de sécurité pour la gestion des limites de crédit
|
||||
|
||||
### Différences avec Notre Module
|
||||
|
||||
1. **Fonctionnalités à Migrer**
|
||||
- [ ] Indicateurs visuels spécifiques pour les clients en dépassement
|
||||
- [ ] Blocage automatique des commandes en dépassement
|
||||
- [ ] Workflow d'approbation personnalisé
|
||||
|
||||
2. **Fonctionnalités à Adapter**
|
||||
- [ ] Utiliser les champs natifs plutôt que nos champs customs
|
||||
- [ ] Intégrer nos règles de blocage avec le système natif
|
||||
- [ ] Adapter les rapports et vues pour utiliser les champs natifs
|
||||
|
||||
## Plan de Migration
|
||||
|
||||
### Phase 1 : Préparation
|
||||
1. **Analyse des Données**
|
||||
- [ ] Identifier les clients avec des limites de crédit
|
||||
- [ ] Mapper les champs actuels vers les champs natifs
|
||||
- [ ] Lister les règles de blocage personnalisées
|
||||
|
||||
2. **Configuration**
|
||||
- [ ] Activer la gestion du crédit dans la configuration de la société
|
||||
- [ ] Configurer les groupes de sécurité appropriés
|
||||
- [ ] Préparer les scripts de migration des données
|
||||
|
||||
### Phase 2 : Migration
|
||||
1. **Migration des Données**
|
||||
- [ ] Transférer les limites de crédit vers le champ natif
|
||||
- [ ] Migrer les configurations de blocage
|
||||
- [ ] Mettre à jour les vues et rapports
|
||||
|
||||
2. **Développement**
|
||||
- [ ] Adapter le code de blocage des commandes
|
||||
- [ ] Implémenter les indicateurs visuels manquants
|
||||
- [ ] Ajouter les fonctionnalités spécifiques non disponibles nativement
|
||||
|
||||
### Phase 3 : Tests
|
||||
1. **Validation Fonctionnelle**
|
||||
- [ ] Tester les limites de crédit
|
||||
- [ ] Vérifier le blocage des commandes
|
||||
- [ ] Valider les workflows d'approbation
|
||||
|
||||
2. **Tests d'Intégration**
|
||||
- [ ] Tester avec les autres modules
|
||||
- [ ] Vérifier la compatibilité avec les processus existants
|
||||
|
||||
## État de la Migration
|
||||
🟡 En cours d'analyse - Utilisation partielle des fonctionnalités natives
|
||||
|
||||
## Notes Importantes
|
||||
- La gestion du crédit est maintenant une fonctionnalité native d'Odoo
|
||||
- Certaines fonctionnalités spécifiques devront être maintenues
|
||||
- L'approche recommandée est d'utiliser au maximum les fonctionnalités natives et de ne conserver que les extensions nécessaires
|
||||
|
||||
## Prochaines Étapes
|
||||
1. Valider l'approche avec l'équipe
|
||||
2. Créer les scripts de migration des données
|
||||
3. Développer les fonctionnalités manquantes
|
||||
4. Planifier la formation des utilisateurs
|
||||
|
|
@ -4,9 +4,13 @@ from odoo import models, fields, api, _
|
|||
class FollowUpReport(models.AbstractModel):
|
||||
_inherit = 'account.followup.report'
|
||||
|
||||
def _get_line_info(self, followup_line):
|
||||
res = super()._get_line_info(followup_line)
|
||||
def _get_followup_report_options(self, partner, options=None):
|
||||
"""
|
||||
Override to include credit hold information in followup report options.
|
||||
"""
|
||||
res = super()._get_followup_report_options(partner, options)
|
||||
res.update({
|
||||
'credit_hold': followup_line.account_hold
|
||||
'credit_hold': partner.followup_line_id.account_hold if partner.followup_line_id else False,
|
||||
'partner_on_hold': partner.on_hold
|
||||
})
|
||||
return res
|
||||
|
|
|
|||
|
|
@ -26,24 +26,21 @@ class Partner(models.Model):
|
|||
compute_sudo=True,
|
||||
)
|
||||
|
||||
@api.depends("postpone_hold_until", "hold_bg")
|
||||
@api.depends("postpone_hold_until", "hold_bg", "commercial_partner_id.hold_bg")
|
||||
def _compute_on_hold(self):
|
||||
# manually re-compute hold_bg since followup_status doesn't get updated in Python but gets recalculated
|
||||
# by an SQL query every time
|
||||
self._compute_hold_bg()
|
||||
for rec in self:
|
||||
# If the parent company is on hold, so are all its sub-contacts and subsidiaries
|
||||
if rec.commercial_partner_id and rec.commercial_partner_id.on_hold:
|
||||
rec.on_hold = True
|
||||
return
|
||||
if rec.commercial_partner_id != rec and rec.commercial_partner_id.hold_bg:
|
||||
if not (rec.commercial_partner_id.postpone_hold_until and rec.commercial_partner_id.postpone_hold_until > date.today()):
|
||||
rec.on_hold = True
|
||||
continue
|
||||
|
||||
# If there is no parent company or the parent is not on hold, we compute for ourselves
|
||||
if rec.hold_bg and not (
|
||||
rec.postpone_hold_until and rec.postpone_hold_until > date.today()
|
||||
):
|
||||
rec.on_hold = True
|
||||
else:
|
||||
if rec.on_hold:
|
||||
rec.message_post(_("Credit hold lifted."))
|
||||
rec.on_hold = False
|
||||
|
||||
@api.autovacuum
|
||||
|
|
@ -61,12 +58,13 @@ class Partner(models.Model):
|
|||
rec.hold_bg = False
|
||||
rec.message_post(body=_("Credit hold lifted."))
|
||||
|
||||
def _execute_followup_partner(self, options=None):
|
||||
res = super()._execute_followup_partner(options)
|
||||
if self.followup_status == "in_need_of_action":
|
||||
if self.followup_line_id.account_hold:
|
||||
self.action_credit_hold()
|
||||
return res
|
||||
@api.model
|
||||
def _get_first_followup_level(self):
|
||||
return self.env["account_followup.followup.line"].search(
|
||||
[("company_id", "parent_of", self.env.company.id)],
|
||||
order="delay asc",
|
||||
limit=1,
|
||||
)
|
||||
|
||||
@api.depends("followup_status", "followup_line_id")
|
||||
def _compute_hold_bg(self):
|
||||
|
|
@ -78,3 +76,33 @@ class Partner(models.Model):
|
|||
rec.hold_bg = False
|
||||
else:
|
||||
rec.hold_bg = prev_hold_bg
|
||||
|
||||
def _get_followup_report(self, options):
|
||||
# Override to prevent hanging on PDF generation
|
||||
# Just set minimal required options without generating the report
|
||||
options.setdefault('attachment_ids', [])
|
||||
options['report_attachment_id'] = False
|
||||
|
||||
def _execute_followup_partner(self, options=None):
|
||||
# Check if we need to place on credit hold before expensive operations
|
||||
should_hold = (
|
||||
self.followup_status == "in_need_of_action" and
|
||||
self.followup_line_id and
|
||||
hasattr(self.followup_line_id, 'account_hold') and
|
||||
self.followup_line_id.account_hold
|
||||
)
|
||||
|
||||
# If this is just for credit hold and we don't need reports/emails, skip heavy operations
|
||||
if options and options.get('credit_hold_only'):
|
||||
if should_hold:
|
||||
self.action_credit_hold()
|
||||
return should_hold
|
||||
|
||||
# Otherwise run the full followup process
|
||||
res = super()._execute_followup_partner(options)
|
||||
|
||||
# Apply credit hold after successful followup execution
|
||||
if should_hold:
|
||||
self.action_credit_hold()
|
||||
|
||||
return res
|
||||
|
|
|
|||
|
|
@ -9,9 +9,8 @@ class SaleOrder(models.Model):
|
|||
help="Whether or not a client has been put on hold due to unpaid invoices.",
|
||||
related="partner_id.on_hold")
|
||||
|
||||
@api.depends('client_on_hold')
|
||||
def action_confirm(self):
|
||||
if any(self.mapped('client_on_hold')):
|
||||
raise UserError(_("This client is on credit hold. No new orders can be confirmed until past-due invoices "
|
||||
"are paid or the accounting team postpones the hold."))
|
||||
super().action_confirm()
|
||||
return super().action_confirm()
|
||||
|
|
|
|||
1
account_credit_hold/tests/__init__.py
Normal file
1
account_credit_hold/tests/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import test_account_credit_hold
|
||||
342
account_credit_hold/tests/test_account_credit_hold.py
Normal file
342
account_credit_hold/tests/test_account_credit_hold.py
Normal file
|
|
@ -0,0 +1,342 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import date, timedelta
|
||||
from odoo.tests import common, tagged, Form
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestAccountCreditHold(common.TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
# Create test partner
|
||||
self.partner = self.env["res.partner"].create(
|
||||
{
|
||||
"name": "Test Customer",
|
||||
"is_company": True,
|
||||
"customer_rank": 1,
|
||||
"email": "test@example.com",
|
||||
}
|
||||
)
|
||||
|
||||
# Try to find existing followup lines or create new ones with unique delays
|
||||
self.followup_line = self.env["account_followup.followup.line"].search(
|
||||
[("delay", "=", 15), ("company_id", "=", self.env.company.id)], limit=1
|
||||
)
|
||||
|
||||
if not self.followup_line:
|
||||
# Find a unique delay value
|
||||
existing_delays = (
|
||||
self.env["account_followup.followup.line"]
|
||||
.search([("company_id", "=", self.env.company.id)])
|
||||
.mapped("delay")
|
||||
)
|
||||
|
||||
delay = 15
|
||||
while delay in existing_delays:
|
||||
delay += 1
|
||||
|
||||
self.followup_line = self.env["account_followup.followup.line"].create(
|
||||
{
|
||||
"name": "First Reminder",
|
||||
"delay": delay,
|
||||
"account_hold": True,
|
||||
"send_email": True,
|
||||
"company_id": self.env.company.id,
|
||||
}
|
||||
)
|
||||
else:
|
||||
# Update existing line to have account_hold
|
||||
self.followup_line.account_hold = True
|
||||
|
||||
# Create followup line without credit hold
|
||||
self.followup_line_no_hold = self.env["account_followup.followup.line"].search(
|
||||
[("delay", "=", 30), ("company_id", "=", self.env.company.id)], limit=1
|
||||
)
|
||||
|
||||
if not self.followup_line_no_hold:
|
||||
# Find a unique delay value
|
||||
existing_delays = (
|
||||
self.env["account_followup.followup.line"]
|
||||
.search([("company_id", "=", self.env.company.id)])
|
||||
.mapped("delay")
|
||||
)
|
||||
|
||||
delay = 30
|
||||
while delay in existing_delays:
|
||||
delay += 1
|
||||
|
||||
self.followup_line_no_hold = self.env[
|
||||
"account_followup.followup.line"
|
||||
].create(
|
||||
{
|
||||
"name": "Second Reminder",
|
||||
"delay": delay,
|
||||
"account_hold": False,
|
||||
"send_email": True,
|
||||
"company_id": self.env.company.id,
|
||||
}
|
||||
)
|
||||
else:
|
||||
# Update existing line to not have account_hold
|
||||
self.followup_line_no_hold.account_hold = False
|
||||
|
||||
def test_credit_hold_basic_functionality(self):
|
||||
"""Test basic credit hold functionality"""
|
||||
# Initially partner should not be on hold
|
||||
self.assertFalse(self.partner.on_hold)
|
||||
self.assertFalse(self.partner.hold_bg)
|
||||
|
||||
# Place partner on credit hold
|
||||
with Form(self.partner) as form:
|
||||
form.record.action_credit_hold()
|
||||
self.assertTrue(self.partner.hold_bg)
|
||||
self.assertTrue(self.partner.on_hold)
|
||||
|
||||
# Lift credit hold
|
||||
with Form(self.partner) as form:
|
||||
form.record.action_lift_credit_hold()
|
||||
self.assertFalse(self.partner.hold_bg)
|
||||
self.assertFalse(self.partner.on_hold)
|
||||
|
||||
def test_postpone_hold_functionality(self):
|
||||
"""Test postpone hold until functionality"""
|
||||
# Place partner on hold
|
||||
with Form(self.partner) as form:
|
||||
form.record.action_credit_hold()
|
||||
self.assertTrue(self.partner.on_hold)
|
||||
|
||||
# Set postpone date to tomorrow
|
||||
tomorrow = date.today() + timedelta(days=1)
|
||||
self.partner.postpone_hold_until = tomorrow
|
||||
|
||||
# Partner should not be on hold due to postponement
|
||||
self.assertFalse(self.partner.on_hold)
|
||||
|
||||
# Set postpone date to yesterday
|
||||
yesterday = date.today() - timedelta(days=1)
|
||||
self.partner.postpone_hold_until = yesterday
|
||||
|
||||
# Partner should be on hold again
|
||||
self.assertTrue(self.partner.on_hold)
|
||||
|
||||
def test_commercial_partner_hold_inheritance(self):
|
||||
"""Test that child contacts inherit hold status from commercial partner"""
|
||||
# Create child contact
|
||||
child_partner = self.env["res.partner"].create(
|
||||
{
|
||||
"name": "Child Contact",
|
||||
"parent_id": self.partner.id,
|
||||
"type": "contact",
|
||||
}
|
||||
)
|
||||
|
||||
# Place parent on hold
|
||||
with Form(self.partner) as form:
|
||||
form.record.action_credit_hold()
|
||||
|
||||
# Child should also be on hold
|
||||
self.assertTrue(child_partner.on_hold)
|
||||
|
||||
# Lift hold from parent
|
||||
self.partner.action_lift_credit_hold()
|
||||
|
||||
# Child should no longer be on hold
|
||||
self.assertFalse(child_partner.on_hold)
|
||||
|
||||
def test_sale_order_blocking(self):
|
||||
"""Test that sale orders are blocked when customer is on credit hold"""
|
||||
# Get or create a product for testing
|
||||
product = self.env["product.product"].search([("type", "=", "consu")], limit=1)
|
||||
if not product:
|
||||
product = self.env["product.product"].create(
|
||||
{
|
||||
"name": "Test Product",
|
||||
"type": "consu",
|
||||
"list_price": 100.0,
|
||||
}
|
||||
)
|
||||
|
||||
# Create a sale order
|
||||
sale_order = self.env["sale.order"].create(
|
||||
{
|
||||
"partner_id": self.partner.id,
|
||||
"order_line": [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": product.id,
|
||||
"product_uom_qty": 1,
|
||||
"price_unit": 100.0,
|
||||
},
|
||||
)
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
# Should be able to confirm when not on hold
|
||||
sale_order.action_confirm()
|
||||
self.assertEqual(sale_order.state, "sale")
|
||||
|
||||
# Create another order and place customer on hold
|
||||
sale_order2 = self.env["sale.order"].create(
|
||||
{
|
||||
"partner_id": self.partner.id,
|
||||
"order_line": [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": product.id,
|
||||
"product_uom_qty": 1,
|
||||
"price_unit": 100.0,
|
||||
},
|
||||
)
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
with Form(self.partner) as form:
|
||||
form.record.action_credit_hold()
|
||||
|
||||
# Should raise error when trying to confirm
|
||||
with self.assertRaises(UserError):
|
||||
sale_order2.action_confirm()
|
||||
|
||||
def test_followup_integration(self):
|
||||
"""Test integration with followup system"""
|
||||
# Set partner to in_need_of_action status and assign followup line
|
||||
self.partner.write(
|
||||
{
|
||||
"followup_status": "in_need_of_action",
|
||||
"followup_line_id": self.followup_line.id,
|
||||
}
|
||||
)
|
||||
|
||||
# Execute followup - should place on hold
|
||||
self.partner._execute_followup_partner()
|
||||
self.assertTrue(self.partner.hold_bg)
|
||||
|
||||
# Test with followup line that doesn't have account_hold
|
||||
self.partner.write(
|
||||
{
|
||||
"followup_line_id": self.followup_line_no_hold.id,
|
||||
"hold_bg": False, # Reset hold status
|
||||
}
|
||||
)
|
||||
|
||||
# Execute followup - should not place on hold
|
||||
self.partner._execute_followup_partner()
|
||||
self.assertFalse(self.partner.hold_bg)
|
||||
|
||||
def test_followup_report_options(self):
|
||||
"""Test that followup report includes credit hold information"""
|
||||
# Set up partner with followup line
|
||||
self.partner.write(
|
||||
{
|
||||
"followup_line_id": self.followup_line.id,
|
||||
}
|
||||
)
|
||||
self.partner.action_credit_hold()
|
||||
|
||||
# Get followup report options
|
||||
report = self.env["account.followup.report"]
|
||||
options = report._get_followup_report_options(self.partner)
|
||||
|
||||
# Should include credit hold information
|
||||
self.assertTrue(options.get("credit_hold"))
|
||||
self.assertTrue(options.get("partner_on_hold"))
|
||||
|
||||
def test_cleanup_expired_hold_postponements(self):
|
||||
"""Test automatic cleanup of expired hold postponements"""
|
||||
# Set expired postponement date
|
||||
expired_date = date.today() - timedelta(days=5)
|
||||
self.partner.postpone_hold_until = expired_date
|
||||
|
||||
# Run cleanup
|
||||
self.env["res.partner"]._cleanup_expired_hold_postponements()
|
||||
|
||||
# Postponement should be cleared
|
||||
self.assertFalse(self.partner.postpone_hold_until)
|
||||
|
||||
def test_hold_bg_computation(self):
|
||||
"""Test hold_bg field computation based on followup status"""
|
||||
# Test with no_action_needed status
|
||||
self.partner.write(
|
||||
{
|
||||
"followup_status": "no_action_needed",
|
||||
"followup_line_id": False,
|
||||
}
|
||||
)
|
||||
self.partner._compute_hold_bg()
|
||||
self.assertFalse(self.partner.hold_bg)
|
||||
|
||||
# Test with followup status and line
|
||||
self.partner.write(
|
||||
{
|
||||
"followup_status": "in_need_of_action",
|
||||
"followup_line_id": self.followup_line.id,
|
||||
"hold_bg": True, # Set initial state
|
||||
}
|
||||
)
|
||||
self.partner._compute_hold_bg()
|
||||
# Should preserve existing hold_bg value when there's a followup line
|
||||
self.assertTrue(self.partner.hold_bg)
|
||||
|
||||
def test_stock_picking_credit_hold_display(self):
|
||||
"""Test that stock pickings show credit hold status"""
|
||||
# Get warehouse and its outgoing picking type
|
||||
warehouse = self.env["stock.warehouse"].search([], limit=1)
|
||||
picking_type = warehouse.out_type_id
|
||||
|
||||
# Create a stock picking
|
||||
picking = self.env["stock.picking"].create(
|
||||
{
|
||||
"partner_id": self.partner.id,
|
||||
"picking_type_id": picking_type.id,
|
||||
"location_id": picking_type.default_location_src_id.id,
|
||||
"location_dest_id": picking_type.default_location_dest_id.id,
|
||||
}
|
||||
)
|
||||
|
||||
# Initially should not show as on hold
|
||||
self.assertFalse(picking.client_on_hold)
|
||||
|
||||
# Place partner on hold
|
||||
with Form(self.partner) as form:
|
||||
form.record.action_credit_hold()
|
||||
|
||||
# Picking should now show as on hold
|
||||
self.assertTrue(picking.client_on_hold)
|
||||
|
||||
def test_get_first_followup_level(self):
|
||||
"""Test _get_first_followup_level method"""
|
||||
first_level = self.partner._get_first_followup_level()
|
||||
self.assertEqual(first_level, self.followup_line)
|
||||
|
||||
# Create an earlier followup level with unique delay
|
||||
existing_delays = (
|
||||
self.env["account_followup.followup.line"]
|
||||
.search([("company_id", "=", self.env.company.id)])
|
||||
.mapped("delay")
|
||||
)
|
||||
|
||||
delay = 5
|
||||
while delay in existing_delays:
|
||||
delay += 1
|
||||
|
||||
earlier_line = self.env["account_followup.followup.line"].create(
|
||||
{
|
||||
"name": "Early Reminder",
|
||||
"delay": delay,
|
||||
"account_hold": False,
|
||||
"company_id": self.env.company.id,
|
||||
}
|
||||
)
|
||||
|
||||
first_level = self.partner._get_first_followup_level()
|
||||
self.assertEqual(first_level, earlier_line)
|
||||
|
|
@ -12,32 +12,22 @@
|
|||
<field name="account_hold" />
|
||||
</xpath></field>
|
||||
</record>
|
||||
<record id="customer_statements_form_view_inherit" model="ir.ui.view">
|
||||
<field name="name">customer.statements.form.view.inherit</field>
|
||||
<field name="model">res.partner</field>
|
||||
|
||||
<record id="manual_reminder_view_form_inherit" model="ir.ui.view">
|
||||
<field name="name">account_credit_hold.manual_reminder.form.inherit</field>
|
||||
<field name="model">account_followup.manual_reminder</field>
|
||||
<field
|
||||
name="inherit_id"
|
||||
ref="account_followup.customer_statements_form_view"
|
||||
ref="account_followup.manual_reminder_view_form"
|
||||
/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//button[last()]" position="after">
|
||||
<field invisible="1" name="hold_bg" />
|
||||
<button
|
||||
class="button btn-secondary"
|
||||
invisible="hold_bg == True"
|
||||
name="action_credit_hold"
|
||||
string="Credit Hold"
|
||||
type="object"
|
||||
/>
|
||||
<button
|
||||
class="button btn-secondary"
|
||||
invisible="hold_bg == False"
|
||||
name="action_lift_credit_hold"
|
||||
string="Lift Credit Hold"
|
||||
type="object"
|
||||
/>
|
||||
<xpath expr="//footer" position="before">
|
||||
<div class="alert alert-warning" role="alert" invisible="not partner_id.on_hold">
|
||||
<strong>Credit Hold:</strong> This customer is currently on credit hold.
|
||||
</div>
|
||||
</xpath></field>
|
||||
</record>
|
||||
|
||||
<record id="action_credit_hold" model="ir.actions.server">
|
||||
<field name="name">action_credit_hold</field>
|
||||
<field name="model_id" ref="base.model_res_partner" />
|
||||
|
|
|
|||
|
|
@ -17,16 +17,16 @@
|
|||
# DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
{
|
||||
'name': 'Aged Partner Balance (North American Style)',
|
||||
'version': '17.0.1.0.0',
|
||||
'summary': 'Present aged partner balance as predictive rather than past due.',
|
||||
'category': 'Accounting',
|
||||
'author': 'Bemade Inc.',
|
||||
'website': 'http://www.bemade.org',
|
||||
'license': 'LGPL-3',
|
||||
'depends': ['account_reports'],
|
||||
'assets': {},
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
'post_init_hook': 'post_init',
|
||||
"name": "Aged Partner Balance (North American Style)",
|
||||
"version": "18.0.1.0.0",
|
||||
"summary": "Present aged partner balance as predictive rather than past due.",
|
||||
"category": "Accounting",
|
||||
"author": "Bemade Inc.",
|
||||
"website": "http://www.bemade.org",
|
||||
"license": "LGPL-3",
|
||||
"depends": ["account_reports"],
|
||||
"assets": {},
|
||||
"installable": True,
|
||||
"auto_install": False,
|
||||
"post_init_hook": "post_init",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from odoo import models, fields
|
||||
from odoo.tools import SQL
|
||||
from itertools import chain
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
|
|
@ -57,13 +58,14 @@ class AgedPartnerBalanceCustomHandler(models.AbstractModel):
|
|||
return fields.Date.to_string(date_obj - relativedelta(days=days))
|
||||
|
||||
date_to = fields.Date.from_string(options['date']['date_to'])
|
||||
# North American style: show future due dates instead of past due
|
||||
periods = [
|
||||
(False, minus_days(date_to, 1)),
|
||||
(date_to, plus_days(date_to, 29)),
|
||||
(plus_days(date_to, 30), plus_days(date_to, 59)),
|
||||
(plus_days(date_to, 60), plus_days(date_to, 89)),
|
||||
(plus_days(date_to, 90), plus_days(date_to, 119)),
|
||||
(plus_days(date_to, 120), False),
|
||||
(False, minus_days(date_to, 1)), # Overdue
|
||||
(date_to, plus_days(date_to, 29)), # 0-29 days
|
||||
(plus_days(date_to, 30), plus_days(date_to, 59)), # 30-59 days
|
||||
(plus_days(date_to, 60), plus_days(date_to, 89)), # 60-89 days
|
||||
(plus_days(date_to, 90), plus_days(date_to, 119)), # 90-119 days
|
||||
(plus_days(date_to, 120), False), # 120+ days
|
||||
]
|
||||
|
||||
def build_result_dict(report, query_res_lines):
|
||||
|
|
@ -77,7 +79,6 @@ class AgedPartnerBalanceCustomHandler(models.AbstractModel):
|
|||
if current_groupby == 'id':
|
||||
query_res = query_res_lines[0] # We're grouping by id, so there is only 1 element in query_res_lines anyway
|
||||
currency = self.env['res.currency'].browse(query_res['currency_id'][0]) if len(query_res['currency_id']) == 1 else None
|
||||
expected_date = len(query_res['expected_date']) == 1 and query_res['expected_date'][0] or len(query_res['due_date']) == 1 and query_res['due_date'][0]
|
||||
rslt.update({
|
||||
'invoice_date': query_res['invoice_date'][0] if len(query_res['invoice_date']) == 1 else None,
|
||||
'due_date': query_res['due_date'][0] if len(query_res['due_date']) == 1 else None,
|
||||
|
|
@ -85,7 +86,6 @@ class AgedPartnerBalanceCustomHandler(models.AbstractModel):
|
|||
'currency_id': query_res['currency_id'][0] if len(query_res['currency_id']) == 1 else None,
|
||||
'currency': currency.display_name if currency else None,
|
||||
'account_name': query_res['account_name'][0] if len(query_res['account_name']) == 1 else None,
|
||||
'expected_date': expected_date or None,
|
||||
'total': None,
|
||||
'has_sublines': query_res['aml_count'] > 0,
|
||||
|
||||
|
|
@ -100,74 +100,78 @@ class AgedPartnerBalanceCustomHandler(models.AbstractModel):
|
|||
'currency_id': None,
|
||||
'currency': None,
|
||||
'account_name': None,
|
||||
'expected_date': None,
|
||||
'total': sum(rslt[f'period{i}'] for i in range(len(periods))),
|
||||
'has_sublines': False,
|
||||
})
|
||||
|
||||
return rslt
|
||||
|
||||
# Build period table
|
||||
# Build period table using SQL class
|
||||
period_table_format = ('(VALUES %s)' % ','.join("(%s, %s, %s)" for period in periods))
|
||||
params = list(chain.from_iterable(
|
||||
(period[0] or None, period[1] or None, i)
|
||||
for i, period in enumerate(periods)
|
||||
))
|
||||
period_table = self.env.cr.mogrify(period_table_format, params).decode(self.env.cr.connection.encoding)
|
||||
period_table = SQL(period_table_format, *params)
|
||||
|
||||
# Build query
|
||||
tables, where_clause, where_params = report._query_get(options, 'strict_range', domain=[('account_id.account_type', '=', internal_type)])
|
||||
# Build query using new Odoo 18.0 methods
|
||||
query = report._get_report_query(options, 'strict_range', domain=[('account_id.account_type', '=', internal_type)])
|
||||
account_alias = query.left_join(lhs_alias='account_move_line', lhs_column='account_id', rhs_table='account_account', rhs_column='id', link='account_id')
|
||||
account_code = self.env['account.account']._field_to_sql(account_alias, 'code', query)
|
||||
|
||||
currency_table = report._get_query_currency_table(options)
|
||||
always_present_groupby = "period_table.period_index, currency_table.rate, currency_table.precision"
|
||||
always_present_groupby = SQL("period_table.period_index")
|
||||
if current_groupby:
|
||||
select_from_groupby = f"account_move_line.{current_groupby} AS grouping_key,"
|
||||
groupby_clause = f"account_move_line.{current_groupby}, {always_present_groupby}"
|
||||
groupby_field_sql = self.env['account.move.line']._field_to_sql("account_move_line", current_groupby, query)
|
||||
select_from_groupby = SQL("%s AS grouping_key,", groupby_field_sql)
|
||||
groupby_clause = SQL("%s, %s", groupby_field_sql, always_present_groupby)
|
||||
else:
|
||||
select_from_groupby = ''
|
||||
select_from_groupby = SQL()
|
||||
groupby_clause = always_present_groupby
|
||||
select_period_query = ','.join(
|
||||
f"""
|
||||
CASE WHEN period_table.period_index = {i}
|
||||
THEN %s * (
|
||||
SUM(ROUND(account_move_line.balance * currency_table.rate, currency_table.precision))
|
||||
- COALESCE(SUM(ROUND(part_debit.amount * currency_table.rate, currency_table.precision)), 0)
|
||||
+ COALESCE(SUM(ROUND(part_credit.amount * currency_table.rate, currency_table.precision)), 0)
|
||||
)
|
||||
ELSE 0 END AS period{i}
|
||||
"""
|
||||
|
||||
multiplicator = -1 if internal_type == 'liability_payable' else 1
|
||||
select_period_query = SQL(',').join(
|
||||
SQL("""
|
||||
CASE WHEN period_table.period_index = %(period_index)s
|
||||
THEN %(multiplicator)s * SUM(%(balance_select)s)
|
||||
ELSE 0 END AS %(column_name)s
|
||||
""",
|
||||
period_index=i,
|
||||
multiplicator=multiplicator,
|
||||
column_name=SQL.identifier(f"period{i}"),
|
||||
balance_select=report._currency_table_apply_rate(SQL(
|
||||
"account_move_line.balance - COALESCE(part_debit.amount, 0) + COALESCE(part_credit.amount, 0)"
|
||||
)),
|
||||
)
|
||||
for i in range(len(periods))
|
||||
)
|
||||
|
||||
tail_query, tail_params = report._get_engine_query_tail(offset, limit)
|
||||
query = f"""
|
||||
WITH period_table(date_start, date_stop, period_index) AS ({period_table})
|
||||
tail_query = report._get_engine_query_tail(offset, limit)
|
||||
query = SQL(
|
||||
"""
|
||||
WITH period_table(date_start, date_stop, period_index) AS (%(period_table)s)
|
||||
|
||||
SELECT
|
||||
{select_from_groupby}
|
||||
%s * (
|
||||
%(select_from_groupby)s
|
||||
%(multiplicator)s * (
|
||||
SUM(account_move_line.amount_currency)
|
||||
- COALESCE(SUM(part_debit.debit_amount_currency), 0)
|
||||
+ COALESCE(SUM(part_credit.credit_amount_currency), 0)
|
||||
) AS amount_currency,
|
||||
ARRAY_AGG(DISTINCT account_move_line.partner_id) AS partner_id,
|
||||
ARRAY_AGG(account_move_line.payment_id) AS payment_id,
|
||||
ARRAY_AGG(DISTINCT move.invoice_date) AS invoice_date,
|
||||
ARRAY_AGG(DISTINCT account_move_line.invoice_date) AS invoice_date,
|
||||
ARRAY_AGG(DISTINCT COALESCE(account_move_line.date_maturity, account_move_line.date)) AS report_date,
|
||||
ARRAY_AGG(DISTINCT account_move_line.expected_pay_date) AS expected_date,
|
||||
ARRAY_AGG(DISTINCT account.code) AS account_name,
|
||||
ARRAY_AGG(DISTINCT %(account_code)s) AS account_name,
|
||||
ARRAY_AGG(DISTINCT COALESCE(account_move_line.date_maturity, account_move_line.date)) AS due_date,
|
||||
ARRAY_AGG(DISTINCT account_move_line.currency_id) AS currency_id,
|
||||
COUNT(account_move_line.id) AS aml_count,
|
||||
ARRAY_AGG(account.code) AS account_code,
|
||||
{select_period_query}
|
||||
ARRAY_AGG(%(account_code)s) AS account_code,
|
||||
%(select_period_query)s
|
||||
|
||||
FROM {tables}
|
||||
FROM %(table_references)s
|
||||
|
||||
JOIN account_journal journal ON journal.id = account_move_line.journal_id
|
||||
JOIN account_account account ON account.id = account_move_line.account_id
|
||||
JOIN account_move move ON move.id = account_move_line.move_id
|
||||
JOIN {currency_table} ON currency_table.company_id = account_move_line.company_id
|
||||
%(currency_table_join)s
|
||||
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
|
|
@ -175,7 +179,7 @@ class AgedPartnerBalanceCustomHandler(models.AbstractModel):
|
|||
SUM(part.debit_amount_currency) AS debit_amount_currency,
|
||||
part.debit_move_id
|
||||
FROM account_partial_reconcile part
|
||||
WHERE part.max_date <= %s AND part.debit_move_id = account_move_line.id
|
||||
WHERE part.max_date <= %(date_to)s AND part.debit_move_id = account_move_line.id
|
||||
GROUP BY part.debit_move_id
|
||||
) part_debit ON TRUE
|
||||
|
||||
|
|
@ -185,7 +189,7 @@ class AgedPartnerBalanceCustomHandler(models.AbstractModel):
|
|||
SUM(part.credit_amount_currency) AS credit_amount_currency,
|
||||
part.credit_move_id
|
||||
FROM account_partial_reconcile part
|
||||
WHERE part.max_date <= %s AND part.credit_move_id = account_move_line.id
|
||||
WHERE part.max_date <= %(date_to)s AND part.credit_move_id = account_move_line.id
|
||||
GROUP BY part.credit_move_id
|
||||
) part_credit ON TRUE
|
||||
|
||||
|
|
@ -200,33 +204,35 @@ class AgedPartnerBalanceCustomHandler(models.AbstractModel):
|
|||
OR COALESCE(account_move_line.date_maturity, account_move_line.date) <= DATE(period_table.date_stop)
|
||||
)
|
||||
|
||||
WHERE {where_clause}
|
||||
WHERE %(search_condition)s
|
||||
|
||||
GROUP BY {groupby_clause}
|
||||
GROUP BY %(groupby_clause)s
|
||||
|
||||
HAVING
|
||||
(
|
||||
SUM(ROUND(account_move_line.debit * currency_table.rate, currency_table.precision))
|
||||
- COALESCE(SUM(ROUND(part_debit.amount * currency_table.rate, currency_table.precision)), 0)
|
||||
) != 0
|
||||
OR
|
||||
(
|
||||
SUM(ROUND(account_move_line.credit * currency_table.rate, currency_table.precision))
|
||||
- COALESCE(SUM(ROUND(part_credit.amount * currency_table.rate, currency_table.precision)), 0)
|
||||
) != 0
|
||||
{tail_query}
|
||||
"""
|
||||
ROUND(SUM(%(having_debit)s), %(currency_precision)s) != 0
|
||||
OR ROUND(SUM(%(having_credit)s), %(currency_precision)s) != 0
|
||||
|
||||
multiplicator = -1 if internal_type == 'liability_payable' else 1
|
||||
params = [
|
||||
multiplicator,
|
||||
*([multiplicator] * len(periods)),
|
||||
date_to,
|
||||
date_to,
|
||||
*where_params,
|
||||
*tail_params,
|
||||
]
|
||||
self._cr.execute(query, params)
|
||||
ORDER BY %(groupby_clause)s
|
||||
|
||||
%(tail_query)s
|
||||
""",
|
||||
account_code=account_code,
|
||||
period_table=period_table,
|
||||
select_from_groupby=select_from_groupby,
|
||||
select_period_query=select_period_query,
|
||||
multiplicator=multiplicator,
|
||||
table_references=query.from_clause,
|
||||
currency_table_join=report._currency_table_aml_join(options),
|
||||
date_to=date_to,
|
||||
search_condition=query.where_clause,
|
||||
groupby_clause=groupby_clause,
|
||||
having_debit=report._currency_table_apply_rate(SQL("CASE WHEN account_move_line.balance > 0 THEN account_move_line.balance else 0 END - COALESCE(part_debit.amount, 0)")),
|
||||
having_credit=report._currency_table_apply_rate(SQL("CASE WHEN account_move_line.balance < 0 THEN -account_move_line.balance else 0 END - COALESCE(part_credit.amount, 0)")),
|
||||
currency_precision=self.env.company.currency_id.decimal_places,
|
||||
tail_query=tail_query,
|
||||
)
|
||||
|
||||
self._cr.execute(query)
|
||||
query_res_lines = self._cr.dictfetchall()
|
||||
|
||||
if not current_groupby:
|
||||
|
|
|
|||
22
apply_inventory_prompt_for_reason/__manifest__.py
Normal file
22
apply_inventory_prompt_for_reason/__manifest__.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"name": "Stock Quant Apply Single Inventory with Reason",
|
||||
"author": "Bemade Inc.",
|
||||
"version": "18.0.1.0.0",
|
||||
"category": "Inventory/Inventory",
|
||||
"summary": "Add reason dialog when applying single inventory adjustment",
|
||||
"description": """
|
||||
This module modifies the behavior of the Apply button on stock quants to show
|
||||
the same reason dialog as when using Apply All, ensuring consistency in
|
||||
inventory adjustment tracking.
|
||||
""",
|
||||
"author": "Bemade",
|
||||
"website": "https://bemade.org",
|
||||
"depends": ["stock"],
|
||||
"data": [
|
||||
"views/stock_quant_views.xml",
|
||||
],
|
||||
"installable": True,
|
||||
"auto_install": False,
|
||||
"license": "LGPL-3",
|
||||
"application": False,
|
||||
}
|
||||
1
apply_inventory_prompt_for_reason/models/__init__.py
Normal file
1
apply_inventory_prompt_for_reason/models/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import stock_quant
|
||||
24
apply_inventory_prompt_for_reason/models/stock_quant.py
Normal file
24
apply_inventory_prompt_for_reason/models/stock_quant.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models, _
|
||||
|
||||
|
||||
class StockQuant(models.Model):
|
||||
_inherit = 'stock.quant'
|
||||
|
||||
def action_apply_single_inventory(self):
|
||||
"""
|
||||
New method that opens the same reason dialog as action_apply_all
|
||||
for single inventory adjustments.
|
||||
"""
|
||||
ctx = dict(self.env.context or {}, default_quant_ids=self.ids)
|
||||
view = self.env.ref('stock.stock_inventory_adjustment_name_form_view', False)
|
||||
return {
|
||||
'name': _('Inventory Adjustment Reference / Reason'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'views': [(view.id, 'form')],
|
||||
'res_model': 'stock.inventory.adjustment.name',
|
||||
'target': 'new',
|
||||
'context': ctx,
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_stock_quant_tree_inventory_editable_inherit" model="ir.ui.view">
|
||||
<field name="name">stock.quant.tree.inventory.editable.inherit</field>
|
||||
<field name="model">stock.quant</field>
|
||||
<field name="inherit_id" ref="stock.view_stock_quant_tree_inventory_editable"/>
|
||||
<field name="arch" type="xml">
|
||||
<button name="action_apply_inventory" position="attributes">
|
||||
<attribute name="name">action_apply_single_inventory</attribute>
|
||||
</button>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
"name": "Change default when adding follower",
|
||||
"version": "17.0.0.0.1",
|
||||
"category": "Extra Tools",
|
||||
'summary': 'Change default when adding follower',
|
||||
"description": """
|
||||
Change default when adding follower
|
||||
Send mail false by default
|
||||
""",
|
||||
"author": "Bemade",
|
||||
'website': 'https://www.bemade.org',
|
||||
"depends": [
|
||||
'mail',
|
||||
],
|
||||
"data": [
|
||||
],
|
||||
"auto_install": False,
|
||||
"installable": True,
|
||||
'license': 'OPL-1'
|
||||
}
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
# Migration vers Odoo 18.0 - bemade_add_follower_no_sendmail_default
|
||||
|
||||
## Description du module
|
||||
Ce module modifie le comportement par défaut du wizard d'ajout de followers pour que l'option "Envoyer un email" soit désactivée par défaut.
|
||||
|
||||
## Analyse technique
|
||||
- Dépendances : mail
|
||||
- Modèles modifiés :
|
||||
- mail.wizard.invite : Modification de la valeur par défaut du champ send_mail à False
|
||||
- Implémentation actuelle :
|
||||
- Hérite de mail.wizard.invite
|
||||
- Redéfinit uniquement le champ send_mail avec default=False
|
||||
|
||||
## Alternatives Natives
|
||||
|
||||
### Configuration Système
|
||||
1. Vérifier dans Odoo 18.0 :
|
||||
- Paramètres de configuration du module mail
|
||||
- Paramètres système (ir.config_parameter)
|
||||
- Préférences utilisateur
|
||||
|
||||
### Approches Alternatives
|
||||
1. Configuration par utilisateur :
|
||||
- Ajouter une préférence utilisateur dans res.users
|
||||
- Utiliser cette préférence comme valeur par défaut
|
||||
|
||||
2. Configuration par type de document :
|
||||
- Ajouter un paramètre dans les paramètres de notification par modèle
|
||||
- Permettre une configuration plus granulaire
|
||||
|
||||
## Recommandations pour la Migration
|
||||
|
||||
### Approche "Vanilla First"
|
||||
1. Évaluer les alternatives natives :
|
||||
- [X] Vérifier si Odoo 18.0 a ajouté une configuration similaire
|
||||
- [ ] Explorer les nouvelles fonctionnalités de notification
|
||||
- [ ] Vérifier les paramètres de notification par défaut
|
||||
|
||||
2. Si aucune alternative native n'existe :
|
||||
- [ ] Considérer l'ajout d'une configuration système
|
||||
- [ ] Implémenter une solution plus flexible (par utilisateur ou par type de document)
|
||||
|
||||
### Modifications Techniques
|
||||
1. Si le module est conservé :
|
||||
- [ ] Mettre à jour la version dans __manifest__.py
|
||||
- [ ] Vérifier la compatibilité de l'héritage du wizard
|
||||
- [ ] Vérifier si le champ send_mail existe toujours et a le même comportement
|
||||
- [ ] Adapter le code aux nouvelles conventions Odoo 18.0
|
||||
|
||||
2. Si migration vers une solution native :
|
||||
- [ ] Créer un module de migration pour la transition
|
||||
- [ ] Migrer les configurations existantes
|
||||
- [ ] Prévoir un plan de désactivation du module
|
||||
|
||||
## Fonctionnalité Native dans Odoo 18.0
|
||||
✅ La fonctionnalité existe nativement dans Odoo 18.0 !
|
||||
|
||||
Dans le modèle `mail.wizard.invite` (`mail/wizard/mail_wizard_invite.py`), le champ `notify` est déjà défini avec `default=False` :
|
||||
```python
|
||||
notify = fields.Boolean('Notify Recipients', default=False)
|
||||
```
|
||||
|
||||
## Plan de Migration
|
||||
|
||||
### Actions Requises
|
||||
1. **Désactivation du Module** :
|
||||
- [ ] Désactiver le module avant la migration vers Odoo 18.0
|
||||
- [ ] Vérifier qu'aucun autre module ne dépend de celui-ci
|
||||
- [ ] Informer les utilisateurs que le comportement est maintenant natif
|
||||
|
||||
2. **Vérification** :
|
||||
- [ ] Tester le comportement natif dans Odoo 18.0
|
||||
- [ ] Confirmer que le comportement par défaut est identique
|
||||
- [ ] Documenter tout changement d'interface utilisateur
|
||||
|
||||
## État de la Migration
|
||||
🟢 Pas de migration nécessaire - Utiliser la fonctionnalité native
|
||||
|
||||
## Notes Importantes
|
||||
- Le comportement souhaité (notification désactivée par défaut) est maintenant le comportement standard d'Odoo 18.0
|
||||
- L'interface utilisateur est similaire, utilisant un widget boolean_toggle
|
||||
- Aucune personnalisation supplémentaire n'est nécessaire
|
||||
|
||||
## Prochaines Étapes
|
||||
1. Planifier la désactivation du module
|
||||
2. Informer les utilisateurs du changement
|
||||
3. Retirer le module de la liste des dépendances des autres modules si nécessaire
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from . import mail_wizard_invite
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
# Copyright Bemade.org
|
||||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class MailWizardInviteDefault(models.TransientModel):
|
||||
_inherit = 'mail.wizard.invite'
|
||||
|
||||
send_mail = fields.Boolean(
|
||||
default=False,
|
||||
help="If true, an invitation email will be sent to the recipient"
|
||||
)
|
||||
|
|
@ -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,2 +0,0 @@
|
|||
from . import controllers
|
||||
from . import models
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
#
|
||||
# Bemade Inc.
|
||||
#
|
||||
# Copyright (C) September 2023 Bemade Inc. (<https://www.bemade.org>).
|
||||
# Author: Marc Durepos (Contact : marc@bemade.org)
|
||||
#
|
||||
# This program is under the terms of the Odoo Proprietary License v1.0 (OPL-1)
|
||||
# It is forbidden to publish, distribute, sublicense, or sell copies of the Software
|
||||
# or modified copies of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
# DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
{
|
||||
'name': 'Documents Portal Base',
|
||||
'version': '17.0.1.0.0',
|
||||
'summary': 'Adds documents to the front-end portal.',
|
||||
'category': 'Document Management',
|
||||
'author': 'Bemade Inc.',
|
||||
'website': 'https://www.bemade.org',
|
||||
'license': 'OPL-1',
|
||||
'depends': ['documents', 'portal', 'mail_enterprise', 'im_livechat'],
|
||||
'data': ['views/document_portal_templates.xml'],
|
||||
'demo': [],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
from . import portal
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
from odoo.addons.portal.controllers.portal import CustomerPortal
|
||||
from odoo.http import request, route
|
||||
from odoo.exceptions import AccessError, MissingError
|
||||
from odoo import _
|
||||
|
||||
|
||||
class DocumentCustomerPortal(CustomerPortal):
|
||||
|
||||
def _prepare_home_portal_values(self, counters):
|
||||
rtn = super()._prepare_home_portal_values(counters)
|
||||
domain = self._prepare_documents_domain()
|
||||
rtn['documents_count'] = request.env['documents.document'].search_count(domain)
|
||||
return rtn
|
||||
|
||||
@route('/my/documents', type='http', auth='user', website=True)
|
||||
def portal_my_documents(self, **kwargs):
|
||||
values = self._prepare_portal_layout_values()
|
||||
Documents = request.env['documents.document']
|
||||
domain = self._prepare_documents_domain()
|
||||
documents_count = Documents.search_count(domain)
|
||||
documents = Documents.search(domain)
|
||||
values.update({
|
||||
'documents_count': documents_count,
|
||||
'documents': documents.sudo(),
|
||||
'default_url': '/my/documents',
|
||||
'page_name': 'my_documents',
|
||||
})
|
||||
return request.render("bemade_documents_portal.portal_my_documents", values)
|
||||
|
||||
def _prepare_documents_domain(self):
|
||||
partner = request.env.user.partner_id
|
||||
user = request.env.user
|
||||
"""Helper method intended to be overridden for future modules."""
|
||||
return ['|',
|
||||
('partner_id', '=', partner.id),
|
||||
('owner_id', '=', user.id),
|
||||
]
|
||||
|
||||
def _render_record_template(self, values):
|
||||
""" Override this method to apply a different template for a single document
|
||||
record on the portal. """
|
||||
return request.render("bemade_documents_portal.document_portal_template", values)
|
||||
|
||||
@route('/my/documents/<int:document_id>', type='http', auth='user', website=True)
|
||||
def portal_document_page(self, document_id, download=False, **kwargs):
|
||||
document = request.env['documents.document'].browse(document_id)
|
||||
if not document:
|
||||
raise MissingError(_('This document does not exist.'))
|
||||
if download:
|
||||
return self._download_attachment(document)
|
||||
values={
|
||||
'document': document,
|
||||
'page_name': 'my_documents',
|
||||
'action': document._get_portal_return_action(),
|
||||
}
|
||||
return self._render_record_template(values)
|
||||
|
||||
def _download_attachment(self, document):
|
||||
attachment = document.attachment_id
|
||||
headers = [
|
||||
('content-type', attachment.mimetype),
|
||||
('content-length', attachment.file_size),
|
||||
('content-disposition', f'attachment; filename="{document.name}"')
|
||||
]
|
||||
return request.make_response(attachment.raw, headers)
|
||||
|
|
@ -1 +0,0 @@
|
|||
from . import documents
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
from odoo import models, fields
|
||||
|
||||
|
||||
class Document(models.Model):
|
||||
_name = 'documents.document'
|
||||
_inherit = ['documents.document', 'portal.mixin']
|
||||
|
||||
def _compute_access_url(self):
|
||||
super()._compute_access_url()
|
||||
for document in self:
|
||||
document.access_url = f'/my/documents/{document.id}'
|
||||
|
||||
def _get_portal_return_action(self):
|
||||
""" Return the action used to display documents when returning from customer
|
||||
portal."""
|
||||
self.ensure_one()
|
||||
return self.env.ref('documents.document_action')
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
<template id="portal_my_home" inherit_id="portal.portal_my_home">
|
||||
<xpath expr="//div[hasclass('o_portal_docs')]" position="inside">
|
||||
<t t-call="portal.portal_docs_entry">
|
||||
<t t-set="title">Documents</t>
|
||||
<t t-set="url">/my/documents</t>
|
||||
<t t-set="placeholder_count">documents_count</t>
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
<template id="portal_my_documents" name="My Documents">
|
||||
<t t-call="portal.portal_layout">
|
||||
<t t-call="portal.portal_table">
|
||||
<thead>
|
||||
<tr class="active">
|
||||
<th>Name</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="documents" t-as="document">
|
||||
<tr>
|
||||
<td>
|
||||
<a t-att-href="document.get_portal_url()">
|
||||
<t t-esc="document.name"/>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
<template id="document_portal_template" name="Document Portal Template">
|
||||
<t t-call="portal.portal_layout">
|
||||
<t t-set="o_portal_fullwidth_alert"
|
||||
groups="documents.group_documents_user">
|
||||
<t t-call="portal.portal_back_in_edit_mode">
|
||||
<t t-set="backend_url"
|
||||
t-value="'/web#model=%s&id=%s&action=%s&view_type=form' % (document._name, document.id, action.id)"/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-call="portal.portal_record_layout">
|
||||
<t t-set="card_header">
|
||||
<div class="row no-gutters">
|
||||
<h5 class="mb-1 mb-md-0">
|
||||
<span t-field="document.name"/>
|
||||
</h5>
|
||||
</div>
|
||||
</t>
|
||||
<t t-set="card_body">
|
||||
<!-- Main Document Contents -->
|
||||
<div id="document_content"
|
||||
class="col-12 col-lg justify-content-end w-100 h-100">
|
||||
<div t-if="'image' in document.mimetype"
|
||||
class="o_attachment_preview_img">
|
||||
<img id="attachment_img"
|
||||
class="img img-fluid d-block"
|
||||
t-attf-src="/documents/content/{{document.id}}"/>
|
||||
</div>
|
||||
<iframe t-if="document.mimetype == 'application/pdf'"
|
||||
class="mb48 w-100 min-vh-100"
|
||||
t-attf-src="/web/static/lib/pdfjs/web/viewer.html?file=/documents/content/{{document.id}}&filename={{document.name}}"/>
|
||||
<ul class="list-group list-group-flush flex-wrap flex-row flex-lg-column">
|
||||
<li class="list-group-item flex-grow-1 b-0">
|
||||
<a class="btn btn-secondary btn-block o_download_btn"
|
||||
t-att-href="document.get_portal_url(download=True)">
|
||||
Download</a>
|
||||
</li>
|
||||
<li class="list-group-item flex-grow-1 b-0">
|
||||
<strong class="text-muted">File Size:
|
||||
<t t-call="documents.format_file_size"/>
|
||||
</strong>
|
||||
</li>
|
||||
<li class="list-group-item flex-grow-1 b-0">
|
||||
<strong class="text-muted">File Type:
|
||||
<t t-esc="document.mimetype"/>
|
||||
</strong>
|
||||
</li>
|
||||
<li class="list-group-item flex-grow-1 b-0">
|
||||
<strong class="text-muted">Attachment Type:
|
||||
<t t-esc="document.attachment_type"/>
|
||||
</strong>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
<!-- Chatter -->
|
||||
<div id="document_communication" class="card-body">
|
||||
<h2>History</h2>
|
||||
<t t-call="portal.message_thread">
|
||||
<t t-set="object" t-value="document"/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
<template id="portal_breadcrumbs" inherit_id="portal.portal_breadcrumbs">
|
||||
<xpath expr="//ol[hasclass('o_portal_submenu')]" position="inside">
|
||||
<li t-if="page_name == 'my_documents'"
|
||||
t-attf-class="breadcrumb-item #{'active ' if not document else ''}">
|
||||
<a t-if="document"
|
||||
t-attf-href="/my/documents?{{ keep_query() }}">Documents</a>
|
||||
<t t-else="">Documents</t>
|
||||
</li>
|
||||
<li t-if="document" class="breadcrumb-item active" t-esc="document.name">
|
||||
</li>
|
||||
</xpath>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
# Copyright (C) 2023 Bemade.org
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import models
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
"name": "Fetchmail Only on production environment",
|
||||
"version": "17.0.0.0.1",
|
||||
"category": "Extra Tools",
|
||||
'summary': 'Fetchmail Only on production environment',
|
||||
"description": """
|
||||
Fetchmail Only on production environment
|
||||
""",
|
||||
"author": "Bemade",
|
||||
'website': 'https://www.bemade.org',
|
||||
"depends": [
|
||||
'mail',
|
||||
],
|
||||
"data": [
|
||||
],
|
||||
"auto_install": True,
|
||||
"installable": True,
|
||||
'license': 'OPL-1'
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
# Migration vers Odoo 18.0 - bemade_fetchmail_only_production
|
||||
|
||||
## Description
|
||||
Module restreignant la récupération des emails uniquement en environnement de production
|
||||
|
||||
## Fonctionnalités Ajoutées
|
||||
- Désactivation de fetchmail dans les environnements de test et de développement
|
||||
- Configuration par base de données
|
||||
- Journalisation des tentatives de récupération
|
||||
|
||||
## Modèles et Champs Modifiés
|
||||
- fetchmail.server
|
||||
- Ajout du champ production_only (boolean)
|
||||
- Ajout du champ last_attempt (datetime)
|
||||
|
||||
## 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,4 +0,0 @@
|
|||
# Copyright (C) 2023 Bemade.org
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import fetchmail_server
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
# Copyright (C) 2023 Bemade.org
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
import logging
|
||||
|
||||
# Import the required classes and decorators from Odoo
|
||||
from odoo import api, models
|
||||
from urllib.parse import urlparse
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class fetchmail_server(models.Model):
|
||||
_inherit = 'fetchmail.server'
|
||||
|
||||
@api.model
|
||||
def fetch_mail(self):
|
||||
if urlparse(self.env['ir.config_parameter'].sudo().get_param('web.base.url')).netloc == \
|
||||
urlparse('https://erp.durpro.com/').netloc:
|
||||
return super(fetchmail_server, self).fetch_mail()
|
||||
else:
|
||||
# Add log message
|
||||
_logger.info("Trying to fetch email, current URL don't match with production URL, so we don't fetch email")
|
||||
return True
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
15.0.0.0.1
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* Initial release
|
||||
|
||||
|
||||
16.0.0.0.1
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* Modification for V16 compatibility
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
#
|
||||
# Bemade Inc.
|
||||
#
|
||||
# Copyright (C) 2023-June Bemade Inc. (<https://www.bemade.org>).
|
||||
# Author: Marc Durepos (Contact : marc@bemade.org)
|
||||
#
|
||||
# This program is under the terms of the Odoo Proprietary License v1.0 (OPL-1)
|
||||
# It is forbidden to publish, distribute, sublicense, or sell copies of the Software
|
||||
# or modified copies of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
# DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
{
|
||||
'name': 'Fix Quality Worksheet',
|
||||
'version': '17.0.1.0.0',
|
||||
'summary': 'Fix Quality worksheet bug from Odoo Enterprise',
|
||||
'description': '',
|
||||
'category': 'Quality Control',
|
||||
'author': 'Bemade Inc.',
|
||||
'website': 'http://www.bemade.org',
|
||||
'license': 'OPL-1',
|
||||
'depends': ['quality_control'],
|
||||
'data': ['reports/worksheet_custom_report_templates.xml'],
|
||||
'assets': {},
|
||||
'installable': True,
|
||||
'auto_install': True,
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<template id="worksheet_page" inherit_id="quality_control.worksheet_page">
|
||||
<xpath expr="//span[@t-field='doc.result']" position="replace">
|
||||
<span t-field="doc.measure"/>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
|
|
@ -20,7 +20,7 @@
|
|||
########################################################################################
|
||||
{
|
||||
"name": "Improved Field Service Management",
|
||||
"version": "17.0.0.4.2",
|
||||
"version": "18.0.0.4.3",
|
||||
"summary": (
|
||||
"Adds functionality necessary for managing field service operations at Durpro."
|
||||
),
|
||||
|
|
|
|||
|
|
@ -3,26 +3,19 @@
|
|||
<record id="planning_project_stage_waiting_parts" model="project.task.type">
|
||||
<field name="sequence">2</field>
|
||||
<field name="name">Waiting on Parts</field>
|
||||
<!-- BV: legend_blocked n'existe plus -->
|
||||
<!-- BV: <field name="legend_blocked">Blocked</field>-->
|
||||
<field name="fold" eval="False" />
|
||||
<!-- <field name="is_closed" eval="False"/>-->
|
||||
<field name="project_ids" eval="[(4,ref('industry_fsm.fsm_project'))]" />
|
||||
</record>
|
||||
<record id="planning_project_stage_work_completed" model="project.task.type">
|
||||
<field name="sequence">15</field>
|
||||
<field name="name">Work Executed</field>
|
||||
<!-- BV: <field name="legend_blocked">Blocked</field>-->
|
||||
<field name="fold" eval="False" />
|
||||
<!-- <field name="is_closed" eval="False"/>-->
|
||||
<field name="project_ids" eval="[(4,ref('industry_fsm.fsm_project'))]" />
|
||||
</record>
|
||||
<record id="planning_project_stage_exception" model="project.task.type">
|
||||
<field name="sequence">19</field>
|
||||
<field name="name">Exception</field>
|
||||
<!-- BV: <field name="legend_blocked">Blocked</field>-->
|
||||
<field name="fold" eval="False" />
|
||||
<!-- <field name="is_closed" eval="False"/>-->
|
||||
<field name="project_ids" eval="[(4,ref('industry_fsm.fsm_project'))]" />
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -1,369 +0,0 @@
|
|||
# Improved Field Service Management - Migration vers Odoo 18.0
|
||||
|
||||
## Description
|
||||
Ce module étend les fonctionnalités de gestion des services sur site (Field Service Management) avec des fonctionnalités spécifiques à Durpro.
|
||||
|
||||
## Fonctionnalités Ajoutées
|
||||
### Gestion améliorée des services sur site
|
||||
- **Existe dans Odoo 18.0 ?** : Partiellement
|
||||
- **Différences avec la version native** :
|
||||
- Gestion avancée des équipements et des contacts
|
||||
- Système de modèles de tâches personnalisé
|
||||
- Intégration approfondie avec les commandes de vente
|
||||
- Gestion des visites FSM
|
||||
- Propagation des affectations et des contacts
|
||||
- **Alternatives** :
|
||||
- Utiliser le module FSM standard d'Odoo
|
||||
- Implémenter des fonctionnalités spécifiques via des modules personnalisés
|
||||
|
||||
## Modèles et Champs Modifiés
|
||||
### Modèles impactés
|
||||
- **project.task** :
|
||||
- **Champs ajoutés** :
|
||||
- work_order_contacts : Contacts liés au bon de travail
|
||||
- site_contacts : Contacts sur site
|
||||
- visit_id : Lien vers la visite FSM
|
||||
- relevant_order_lines : Lignes de commande pertinentes
|
||||
- work_order_number : Numéro de bon de travail
|
||||
- propagate_assignment : Propagation des affectations
|
||||
- is_closed : Indicateur de tâche fermée
|
||||
- root_ancestor : Tâche racine de la hiérarchie
|
||||
- **Méthodes modifiées** :
|
||||
- create() : Gestion des contacts et numéros de bon de travail
|
||||
- write() : Propagation des modifications aux sous-tâches
|
||||
- _compute_allow_billable() : Calcul de la facturabilité
|
||||
- _fsm_create_sale_order_line() : Création de lignes de commande
|
||||
- action_fsm_validate() : Validation des tâches FSM
|
||||
- synchronize_name_fsm() : Synchronisation des noms des tâches
|
||||
- **Recommandations de migration** :
|
||||
- Vérifier la compatibilité avec le nouveau système de tâches Odoo 18
|
||||
- Tester la propagation des modifications
|
||||
- Adapter les calculs de facturabilité
|
||||
- Vérifier la gestion des noms des tâches
|
||||
|
||||
- **task.template** :
|
||||
- **Nouveau modèle** : project.task.template
|
||||
- **Champs principaux** :
|
||||
- name : Nom du modèle
|
||||
- description : Description HTML
|
||||
- assignees : Utilisateurs assignés par défaut
|
||||
- customer : Client par défaut
|
||||
- project : Projet par défaut
|
||||
- tags : Tags par défaut
|
||||
- parent : Modèle parent
|
||||
- subtasks : Sous-tâches
|
||||
- sequence : Ordre d'affichage
|
||||
- company_id : Société
|
||||
- planned_hours : Heures planifiées
|
||||
- equipment_ids : Équipements à entretenir
|
||||
- **Méthodes principales** :
|
||||
- _prepare_new_task_values_from_self() : Prépare les valeurs pour une nouvelle tâche
|
||||
- create_task_from_self() : Crée une tâche à partir du modèle
|
||||
- **Recommandations de migration** :
|
||||
- Vérifier la compatibilité avec le nouveau système de modèles de tâches Odoo 18
|
||||
- Tester la création de tâches à partir des modèles
|
||||
- Adapter la gestion des équipements et des heures planifiées
|
||||
- **product.template** :
|
||||
- **Champs ajoutés** :
|
||||
- task_template_id : Modèle de tâche associé
|
||||
- is_field_service : Indicateur de service sur site
|
||||
- **Recommandations de migration** :
|
||||
- Vérifier la compatibilité avec le nouveau système de produits Odoo 18
|
||||
- Tester la gestion des modèles de tâches
|
||||
- Adapter l'indicateur de service sur site
|
||||
- **res.partner** :
|
||||
- **Champs ajoutés** :
|
||||
- is_site_contact : Indicateur de contact sur site
|
||||
- is_service_site : Indicateur de site de service
|
||||
- site_ids : Sites de travail liés
|
||||
- site_contacts : Contacts sur site
|
||||
- work_order_contacts : Destinataires des bons de travail
|
||||
- **Méthodes modifiées** :
|
||||
- _compute_is_site_contact() : Calcul de l'état de contact sur site
|
||||
- _search_is_site_contact() : Recherche des contacts sur site
|
||||
- _compute_is_service_site() : Calcul de l'état de site de service
|
||||
- **Recommandations de migration** :
|
||||
- Vérifier la compatibilité avec le nouveau système de partenaires Odoo 18
|
||||
- Tester les calculs des indicateurs
|
||||
- Adapter la gestion des relations entre sites et contacts
|
||||
- **sale.order** :
|
||||
- **Champs ajoutés** :
|
||||
- valid_equipment_ids : Équipements valides
|
||||
- default_equipment_ids : Équipements par défaut à entretenir
|
||||
- summary_equipment_ids : Équipements en cours de maintenance
|
||||
- site_contacts : Contacts sur site
|
||||
- work_order_contacts : Destinataires du bon de travail
|
||||
- visit_ids : Visites FSM liées
|
||||
- is_fsm : Indicateur de commande FSM
|
||||
- **Méthodes modifiées** :
|
||||
- get_relevant_order_lines() : Récupère les lignes pertinentes pour une tâche
|
||||
- _compute_summary_equipment_ids() : Calcule les équipements en maintenance
|
||||
- _onchange_partner_shipping_id() : Gère les changements de partenaire
|
||||
- _compute_default_contacts() : Calcule les contacts par défaut
|
||||
- _compute_default_equipment() : Calcule les équipements par défaut
|
||||
- copy() : Gère la copie des visites
|
||||
- _create_default_visit() : Crée une visite par défaut
|
||||
- _create_or_organize_visits_if_needed() : Organise les visites FSM
|
||||
- action_confirm() : Confirmation de commande avec gestion FSM
|
||||
- write() : Gère les mises à jour des partenaires
|
||||
- **Recommandations de migration** :
|
||||
- Vérifier la compatibilité avec le nouveau système de commandes Odoo 18
|
||||
- Tester la gestion des équipements
|
||||
- Adapter la gestion des visites FSM
|
||||
- Vérifier les calculs de contacts et d'équipements
|
||||
|
||||
## Vues à modifier
|
||||
- **Vues existantes** :
|
||||
- Formulaire de tâche :
|
||||
- Ajout d'une page "Equipment and Contacts"
|
||||
- Modification des boutons de validation
|
||||
- Ajout du champ propagate_assignment
|
||||
- Vue liste :
|
||||
- Ajout du champ work_order_number
|
||||
- Masquage de certains champs optionnels
|
||||
- Vue calendrier :
|
||||
- Personnalisation des couleurs par utilisateur
|
||||
- Suppression du champ worksheet_template_id
|
||||
- Vue de recherche :
|
||||
- Modification des filtres de planification
|
||||
- Ajout du filtre "Parent Task"
|
||||
- **Nouvelles vues** :
|
||||
- Aucune nouvelle vue créée, uniquement des modifications des vues existantes
|
||||
- **Recommandations pour Odoo 18** :
|
||||
- Vérifier la compatibilité avec les nouvelles vues Odoo 18
|
||||
- Adapter les modifications de vues aux nouveaux designs
|
||||
- Tester les fonctionnalités de recherche et de filtrage
|
||||
- Vérifier la gestion des couleurs dans le calendrier
|
||||
|
||||
## Rapports
|
||||
- **Rapports personnalisés** :
|
||||
- **Nouveaux blocs de rapport** :
|
||||
- Tableau des matériaux
|
||||
- Tableau des temps et matériaux avec tarification
|
||||
- Liste des sous-tâches
|
||||
- Bloc d'informations sur la commande
|
||||
- Résumé des équipements
|
||||
- Entrées de feuille de temps
|
||||
- Bloc de signature
|
||||
- **Modifications principales** :
|
||||
- Intégration des contacts et informations client
|
||||
- Affichage des dates planifiées
|
||||
- Gestion des signatures numériques
|
||||
- Personnalisation de la mise en page
|
||||
- **Recommandations pour Odoo 18** :
|
||||
- Vérifier la compatibilité avec le nouveau système de rapports Odoo 18
|
||||
- Adapter les modèles de rapport aux nouvelles fonctionnalités
|
||||
- Tester l'affichage des différents blocs
|
||||
- Vérifier la gestion des signatures numériques
|
||||
|
||||
## Assistants (Wizards)
|
||||
- **Nouveaux assistants** : À documenter
|
||||
|
||||
## Analyse des Alternatives Natives Odoo 18.0
|
||||
|
||||
### Fonctionnalités Natives à Explorer
|
||||
1. **Gestion des Services** :
|
||||
- Module Industry FSM d'Odoo Enterprise
|
||||
- Nouvelles fonctionnalités de planification
|
||||
- Système de rapports amélioré
|
||||
|
||||
2. **Gestion des Équipements** :
|
||||
- Module Maintenance d'Odoo
|
||||
- Intégration avec FSM
|
||||
- Système de suivi des équipements
|
||||
|
||||
3. **Gestion des Contacts** :
|
||||
- Système de contacts hiérarchiques
|
||||
- Gestion des rôles des contacts
|
||||
- Système d'adresses de livraison
|
||||
|
||||
### Approche "Vanilla First"
|
||||
|
||||
1. **Fonctionnalités à Conserver en Custom** :
|
||||
- Gestion spécifique des visites FSM
|
||||
- Propagation des affectations
|
||||
- Modèles de tâches personnalisés
|
||||
- Gestion avancée des contacts sur site
|
||||
|
||||
2. **Fonctionnalités à Migrer vers Native** :
|
||||
- Utiliser le système de planification natif
|
||||
- Adopter le système de rapports standard
|
||||
- Utiliser la gestion des équipements native
|
||||
- Intégrer avec le système de contacts standard
|
||||
|
||||
## Plan de Migration
|
||||
|
||||
### Phase 1 : Analyse et Préparation
|
||||
1. **Audit des Fonctionnalités** :
|
||||
- [ ] Identifier les fonctionnalités disponibles nativement
|
||||
- [ ] Lister les gaps fonctionnels
|
||||
- [ ] Évaluer l'impact sur les processus existants
|
||||
|
||||
2. **Planification** :
|
||||
- [ ] Définir la stratégie de migration
|
||||
- [ ] Établir un calendrier
|
||||
- [ ] Identifier les risques
|
||||
|
||||
### Phase 2 : Migration Technique
|
||||
1. **Adaptation du Code** :
|
||||
- [ ] Mettre à jour les vues (tree -> list)
|
||||
- [ ] Supprimer les attrs obsolètes
|
||||
- [ ] Adapter les méthodes aux nouvelles API
|
||||
|
||||
2. **Intégration Native** :
|
||||
- [ ] Intégrer avec Industry FSM
|
||||
- [ ] Connecter avec le module Maintenance
|
||||
- [ ] Adapter le système de contacts
|
||||
|
||||
### Phase 3 : Tests et Validation
|
||||
1. **Tests Fonctionnels** :
|
||||
- [ ] Validation des workflows
|
||||
- [ ] Tests des rapports
|
||||
- [ ] Vérification des intégrations
|
||||
|
||||
2. **Tests de Performance** :
|
||||
- [ ] Analyse des requêtes SQL
|
||||
- [ ] Tests de charge
|
||||
- [ ] Optimisation si nécessaire
|
||||
|
||||
## Recommandations Spécifiques
|
||||
|
||||
### Modèles et Champs
|
||||
1. **project.task** :
|
||||
- Utiliser les champs natifs quand possible
|
||||
- Conserver uniquement les champs spécifiques
|
||||
- Adapter les méthodes aux nouvelles API
|
||||
|
||||
2. **task.template** :
|
||||
- Évaluer le système de modèles natif
|
||||
- Simplifier la structure si possible
|
||||
- Optimiser la création de tâches
|
||||
|
||||
3. **sale.order** :
|
||||
- Utiliser les fonctionnalités FSM natives
|
||||
- Optimiser la gestion des visites
|
||||
- Simplifier les calculs
|
||||
|
||||
### Vues et Interface
|
||||
1. **Modifications Prioritaires** :
|
||||
- Remplacer tree par list
|
||||
- Supprimer les attrs obsolètes
|
||||
- Adapter aux nouveaux standards UI
|
||||
|
||||
2. **Améliorations Suggérées** :
|
||||
- Utiliser les nouveaux widgets
|
||||
- Simplifier les vues
|
||||
- Améliorer l'expérience utilisateur
|
||||
|
||||
### Rapports
|
||||
1. **Stratégie de Migration** :
|
||||
- Utiliser le nouveau système de rapports
|
||||
- Adapter les modèles existants
|
||||
- Optimiser le rendu
|
||||
|
||||
## État de la Migration
|
||||
⚪ En analyse préliminaire
|
||||
|
||||
## Notes Importantes
|
||||
- Module complexe nécessitant une approche progressive
|
||||
- Forte dépendance avec d'autres modules
|
||||
- Impact important sur les processus métier
|
||||
- Nécessité de formation des utilisateurs
|
||||
|
||||
## Prochaines Étapes
|
||||
1. Valider l'approche avec les parties prenantes
|
||||
2. Créer un environnement de test
|
||||
3. Commencer par les fonctionnalités critiques
|
||||
4. Planifier la formation des utilisateurs
|
||||
|
||||
## Analyse Technique
|
||||
|
||||
### Fonctionnalités Natives dans Odoo 18.0
|
||||
|
||||
Le module `industry_fsm` d'Odoo Enterprise 18.0 inclut déjà plusieurs fonctionnalités avancées :
|
||||
|
||||
1. **Gestion des Tâches FSM**
|
||||
- Champ `is_fsm` sur les projets pour identifier les projets FSM
|
||||
- Champ `fsm_done` sur les tâches pour marquer leur complétion
|
||||
- Gestion des signatures sur les rapports de travail
|
||||
- Gestion des coordonnées client (téléphone, adresse, etc.)
|
||||
- Planification avec dates de début/fin
|
||||
|
||||
2. **Fonctionnalités de Base**
|
||||
- Vue spécifique pour les travailleurs sur le terrain
|
||||
- Rapports sur les tâches
|
||||
- Intégration avec les feuilles de temps
|
||||
- Géolocalisation des clients
|
||||
- Gestion des produits sur les tâches
|
||||
|
||||
3. **Sécurité et Contraintes**
|
||||
- Règles de sécurité spécifiques FSM
|
||||
- Contraintes sur les projets FSM (company_id requis)
|
||||
- Restrictions sur les dépendances de tâches et les jalons
|
||||
|
||||
### Différences avec Notre Module
|
||||
|
||||
1. **Fonctionnalités à Migrer**
|
||||
- [ ] Fonctionnalités spécifiques de gestion d'équipement
|
||||
- [ ] Workflows personnalisés
|
||||
- [ ] Rapports et analyses spécifiques
|
||||
- [ ] Intégrations avec d'autres modules custom
|
||||
|
||||
2. **Fonctionnalités à Adapter**
|
||||
- [ ] Utiliser les champs natifs plutôt que nos champs customs
|
||||
- [ ] Adapter nos vues aux nouvelles conventions Odoo 18.0
|
||||
- [ ] Intégrer nos processus avec le système natif
|
||||
|
||||
## Plan de Migration
|
||||
|
||||
### Phase 1 : Préparation
|
||||
1. **Analyse des Données**
|
||||
- [ ] Identifier les données spécifiques à notre module
|
||||
- [ ] Mapper les champs actuels vers les champs natifs
|
||||
- [ ] Lister les fonctionnalités uniques à préserver
|
||||
|
||||
2. **Configuration**
|
||||
- [ ] Activer et configurer le module `industry_fsm`
|
||||
- [ ] Vérifier les dépendances et les conflits
|
||||
- [ ] Préparer les scripts de migration des données
|
||||
|
||||
### Phase 2 : Migration
|
||||
1. **Migration des Données**
|
||||
- [ ] Transférer les données vers les structures natives
|
||||
- [ ] Adapter les configurations existantes
|
||||
- [ ] Mettre à jour les vues et rapports
|
||||
|
||||
2. **Développement**
|
||||
- [ ] Adapter le code pour utiliser l'API Odoo 18.0
|
||||
- [ ] Implémenter les fonctionnalités manquantes
|
||||
- [ ] Mettre à jour les vues XML (plus d'attrs, list au lieu de tree)
|
||||
|
||||
### Phase 3 : Tests
|
||||
1. **Validation Fonctionnelle**
|
||||
- [ ] Tester les fonctionnalités de base FSM
|
||||
- [ ] Vérifier nos fonctionnalités spécifiques
|
||||
- [ ] Valider les workflows
|
||||
|
||||
2. **Tests d'Intégration**
|
||||
- [ ] Tester avec les autres modules
|
||||
- [ ] Vérifier la compatibilité mobile
|
||||
- [ ] Valider les performances
|
||||
|
||||
## État de la Migration
|
||||
🟡 En cours d'analyse - Utilisation maximale des fonctionnalités natives
|
||||
|
||||
## Notes Importantes
|
||||
- Le module `industry_fsm` d'Odoo Enterprise offre une base solide
|
||||
- Plusieurs de nos fonctionnalités peuvent être remplacées par des fonctionnalités natives
|
||||
- Certaines personnalisations spécifiques devront être maintenues
|
||||
- La nouvelle interface utilisateur nécessitera une formation des utilisateurs
|
||||
|
||||
## Prochaines Étapes
|
||||
1. Valider l'approche avec l'équipe
|
||||
2. Créer les scripts de migration des données
|
||||
3. Développer les fonctionnalités manquantes
|
||||
4. Planifier la formation des utilisateurs
|
||||
|
||||
## Notes de Version
|
||||
- Version originale: 17.0.1.0.0
|
||||
- Dernière analyse: 26/01/2025
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
from odoo import api, SUPERUSER_ID
|
||||
import logging
|
||||
from openupgradelib.openupgrade import update_module_moved_models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def migrate(cr, version):
|
||||
"""In this version, we separate the bemade_fsm.equipment and its associated models
|
||||
out into a new module named fsm_equipment. We need to move those models and rename
|
||||
some fields."""
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
_logger.info("Moving FSM equipment...")
|
||||
# Move the old equipment over to the new table
|
||||
cr.execute(
|
||||
"""
|
||||
INSERT INTO fsm_equipment
|
||||
(id, code, name, description, partner_id, location_notes, active)
|
||||
SELECT id, pid_tag code, name, description, partner_location_id, location_notes,
|
||||
active
|
||||
FROM bemade_fsm_equipment
|
||||
"""
|
||||
)
|
||||
|
||||
_logger.info("Moving FSM equipment tags...")
|
||||
# Move the tags
|
||||
cr.execute(
|
||||
"""
|
||||
INSERT INTO fsm_equipment_tag (id, name, color)
|
||||
SELECT id, name, color FROM bemade_fsm_equipment_tag
|
||||
"""
|
||||
)
|
||||
|
||||
_logger.info("Re-creating equipment to tag relations.")
|
||||
# Add the relations
|
||||
# Schema | Name | Type | Owner
|
||||
# --------+---------------------------------------------------+-------+-------
|
||||
# public | bemade_fsm_equipment_bemade_fsm_equipment_tag_rel | table | odoo
|
||||
# public | bemade_fsm_equipment_sale_order_line_rel | table | odoo
|
||||
# public | bemade_fsm_equipment_sale_order_rel | table | odoo
|
||||
# public | bemade_fsm_task_equipment_rel | table | odoo
|
||||
# public | bemade_fsm_task_template_equipment_rel | table | odoo
|
||||
# public | fsm_equipment_fsm_equipment_tag_rel | table | odoo
|
||||
# public | fsm_equipment_sale_order_rel | table | odoo
|
||||
# public | fsm_task_equipment_rel | table | odoo
|
||||
|
||||
cr.execute(
|
||||
"""
|
||||
INSERT INTO fsm_task_equipment_rel (equipment_id, task_id)
|
||||
SELECT equipment_id, task_id from bemade_fsm_task_equipment_rel
|
||||
"""
|
||||
)
|
||||
|
||||
cr.execute(
|
||||
"""
|
||||
INSERT INTO fsm_equipment_fsm_equipment_tag_rel (fsm_equipment_id, fsm_equipment_tag_id)
|
||||
SELECT bemade_fsm_equipment_id, bemade_fsm_equipment_tag_id
|
||||
FROM bemade_fsm_equipment_bemade_fsm_equipment_tag_rel
|
||||
"""
|
||||
)
|
||||
|
||||
# Clean up
|
||||
|
||||
_logger.info("Deleting menu items.")
|
||||
cr.execute(
|
||||
"""
|
||||
DELETE FROM ir_ui_menu WHERE id in (
|
||||
SELECT res_id from ir_model_data where model='ir.ui.menu'
|
||||
and module='bemade_fsm'
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
cr.execute(
|
||||
"DELETE FROM ir_model_data where model='ir.ui.menu' and module='bemade_fsm'"
|
||||
)
|
||||
|
||||
cr.execute("DELETE FROM ir_model_fields where model ilike 'bemade_fsm.equipment%'")
|
||||
cr.execute(
|
||||
"DELETE FROM ir_model WHERE name->>'en_US' ilike 'bemade_fsm.equipment%'"
|
||||
)
|
||||
|
|
@ -5,7 +5,8 @@ class SaleOrder(models.Model):
|
|||
_inherit = "sale.order"
|
||||
|
||||
valid_equipment_ids = fields.One2many(
|
||||
comodel_name="fsm.equipment", related="partner_id.owned_equipment_ids"
|
||||
comodel_name="fsm.equipment",
|
||||
related="partner_id.commercial_partner_id.owned_equipment_ids",
|
||||
)
|
||||
|
||||
default_equipment_ids = fields.Many2many(
|
||||
|
|
@ -50,7 +51,6 @@ class SaleOrder(models.Model):
|
|||
store=True,
|
||||
)
|
||||
|
||||
@api.depends("order_line.task_id")
|
||||
def get_relevant_order_lines(self, task_id):
|
||||
self.ensure_one()
|
||||
linked_lines = self.order_line.filtered(
|
||||
|
|
@ -155,9 +155,9 @@ class SaleOrder(models.Model):
|
|||
self._create_or_organize_visits_if_needed()
|
||||
return super().action_confirm()
|
||||
|
||||
def write(self, values):
|
||||
res = super().write(values)
|
||||
if "partner_shipping_id" in values:
|
||||
def write(self, vals):
|
||||
res = super().write(vals)
|
||||
if "partner_shipping_id" in vals:
|
||||
for rec in self:
|
||||
rec.tasks_ids.write({"partner_id": rec.partner_shipping_id.id})
|
||||
return res
|
||||
|
|
|
|||
|
|
@ -72,8 +72,8 @@ class SaleOrderLine(models.Model):
|
|||
rec.is_field_service = rec.product_id.is_field_service
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals):
|
||||
recs = super().create(vals)
|
||||
def create(self, vals_list):
|
||||
recs = super().create(vals_list)
|
||||
for rec in recs:
|
||||
if rec.order_id.default_equipment_ids and not rec.equipment_ids:
|
||||
rec.equipment_ids = rec.order_id.default_equipment_ids
|
||||
|
|
@ -111,14 +111,16 @@ class SaleOrderLine(models.Model):
|
|||
for t in template.subtasks:
|
||||
subtask = _create_task_from_template(project, t, task)
|
||||
subtasks.append(subtask)
|
||||
# task.write({"child_ids": [Command.set([t.id for t in subtasks])]})
|
||||
|
||||
# We don't want to see the sub-tasks on the SO
|
||||
task.child_ids.write(
|
||||
{
|
||||
"sale_order_id": None,
|
||||
"sale_line_id": None,
|
||||
}
|
||||
)
|
||||
if task.child_ids:
|
||||
task.child_ids.write(
|
||||
{
|
||||
"sale_order_id": None,
|
||||
"sale_line_id": None,
|
||||
}
|
||||
)
|
||||
|
||||
return task
|
||||
|
||||
def _timesheet_create_task_prepare_values_from_template(
|
||||
|
|
@ -142,7 +144,11 @@ class SaleOrderLine(models.Model):
|
|||
vals["tag_ids"] = template.tags.ids
|
||||
vals["allocated_hours"] = template.planned_hours
|
||||
vals["sequence"] = template.sequence
|
||||
vals["partner_id"] = self.order_id.partner_id.id
|
||||
# Use shipping address for FSM tasks for consistency
|
||||
if project and project.is_fsm:
|
||||
vals["partner_id"] = self.order_id.partner_shipping_id.id
|
||||
else:
|
||||
vals["partner_id"] = self.order_id.partner_id.id
|
||||
if template.equipment_ids:
|
||||
vals["equipment_ids"] = template.equipment_ids.ids
|
||||
return vals
|
||||
|
|
@ -150,6 +156,9 @@ class SaleOrderLine(models.Model):
|
|||
tmpl = self.product_id.task_template_id
|
||||
if not tmpl:
|
||||
task = super()._timesheet_create_task(project)
|
||||
# For FSM tasks without a template, update partner_id to use shipping address
|
||||
if project.is_fsm and task:
|
||||
task.partner_id = self.order_id.partner_shipping_id.id
|
||||
else:
|
||||
task = _create_task_from_template(project, tmpl, None)
|
||||
self.write({"task_id": task.id})
|
||||
|
|
@ -163,6 +172,7 @@ class SaleOrderLine(models.Model):
|
|||
"product_name": self.product_id.name,
|
||||
}
|
||||
task.message_post(body=task_msg)
|
||||
|
||||
if not task.equipment_ids and self.equipment_ids:
|
||||
task.equipment_ids = self.equipment_ids.ids
|
||||
return task
|
||||
|
|
@ -285,10 +295,10 @@ class SaleOrderLine(models.Model):
|
|||
return val
|
||||
return True
|
||||
|
||||
@api.depends("product_id.detailed_type", "product_id.service_tracking")
|
||||
@api.depends("product_id.type", "product_id.service_tracking")
|
||||
def _compute_is_fsm(self):
|
||||
for rec in self:
|
||||
rec.is_fsm = (
|
||||
rec.product_id.detailed_type == "service"
|
||||
rec.product_id.type == "service"
|
||||
and rec.product_id.service_tracking == "task_global_project"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from odoo import fields, models, api, Command
|
||||
from odoo.addons.project.models.project_task import CLOSED_STATES
|
||||
import re
|
||||
from typing import cast, List
|
||||
|
||||
|
||||
class Task(models.Model):
|
||||
|
|
@ -61,10 +62,11 @@ class Task(models.Model):
|
|||
rec.is_closed = rec.state in CLOSED_STATES
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals):
|
||||
res = super().create(vals)
|
||||
def create(self, vals_list):
|
||||
res = super().create(vals_list)
|
||||
for rec in res:
|
||||
if rec.parent_id and rec.is_fsm:
|
||||
# Always ensure FSM subtasks have a partner_id set from their parent
|
||||
rec.partner_id = rec.parent_id.partner_id
|
||||
if not rec.work_order_contacts and rec.parent_id:
|
||||
rec.work_order_contacts = rec.parent_id.work_order_contacts
|
||||
|
|
@ -78,7 +80,9 @@ class Task(models.Model):
|
|||
)
|
||||
if prev_seqs:
|
||||
pattern = re.compile(r"(\d+)$")
|
||||
matches = map(lambda n: pattern.search(n), prev_seqs)
|
||||
matches = map(
|
||||
lambda n: pattern.search(n), cast(List[str], prev_seqs)
|
||||
)
|
||||
seq += max(map(lambda n: int(n.group(1)) if n else 0, matches))
|
||||
rec.work_order_number = (
|
||||
rec.sale_order_id.name.replace("SO", "SVR", 1) + f"-{seq}"
|
||||
|
|
@ -123,7 +127,11 @@ class Task(models.Model):
|
|||
)
|
||||
if "partner_id" in vals:
|
||||
child_vals.update(partner_id=vals["partner_id"])
|
||||
rec.child_ids.write(child_vals)
|
||||
if "state" in vals and rec.state in CLOSED_STATES:
|
||||
# Propagate task completion or cancelling to subtasks
|
||||
child_vals.update(state=rec.state)
|
||||
if child_vals:
|
||||
rec.child_ids.write(child_vals)
|
||||
return res
|
||||
|
||||
@api.depends("sale_order_id")
|
||||
|
|
@ -135,20 +143,6 @@ class Task(models.Model):
|
|||
or False
|
||||
)
|
||||
|
||||
def _get_closed_stage_by_project(self):
|
||||
"""Gets the stage representing completed tasks for each project in
|
||||
self.project_id. Copied from industry_fsm/.../project.py:217-221
|
||||
for consistency.
|
||||
|
||||
:returns: Dict of project.project -> project.task.type"""
|
||||
return {
|
||||
project: (
|
||||
project.type_ids.filtered(lambda stage: stage.is_closed)[:1]
|
||||
or project.type_ids[-1:]
|
||||
)
|
||||
for project in self.project_id
|
||||
}
|
||||
|
||||
@api.depends("parent_id.visit_id", "project_id.is_fsm", "project_id.allow_billable")
|
||||
def _compute_allow_billable(self):
|
||||
for rec in self:
|
||||
|
|
@ -220,3 +214,44 @@ class Task(models.Model):
|
|||
def _compute_root_ancestor(self):
|
||||
for rec in self:
|
||||
rec.root_ancestor = rec.parent_id and rec.parent_id.root_ancestor or self
|
||||
|
||||
@api.depends(
|
||||
"partner_id",
|
||||
"sale_line_id.order_partner_id",
|
||||
"parent_id.sale_line_id",
|
||||
"project_id.sale_line_id",
|
||||
"milestone_id.sale_line_id",
|
||||
"allow_billable",
|
||||
)
|
||||
def _compute_sale_line(self):
|
||||
"""Override to prevent subtasks from inheriting parent's sale_line_id.
|
||||
|
||||
In the base implementation, if a task and its parent share the same commercial partner,
|
||||
the task will inherit the parent's sale_line_id. This causes issues with our FSM tasks
|
||||
where we explicitly want subtasks to NOT have a sale_line_id set.
|
||||
"""
|
||||
|
||||
# Only run on root tasks
|
||||
subtasks = self.filtered("parent_id")
|
||||
(subtasks - subtasks.filtered("sale_line_id")).sale_line_id = False
|
||||
super(Task, self - subtasks)._compute_sale_line()
|
||||
|
||||
@api.depends("parent_id.partner_id", "project_id")
|
||||
def _compute_partner_id(self):
|
||||
"""Override to prevent clearing partner_id for FSM tasks.
|
||||
|
||||
In the base implementation, if a task has a partner_id but no project_id or parent_id,
|
||||
the partner_id is cleared. This causes issues with our FSM tasks where we want to
|
||||
preserve the partner_id even if project_id or parent_id is temporarily not set.
|
||||
"""
|
||||
# Only run the standard logic on non-FSM tasks
|
||||
non_fsm_tasks = self.filtered(lambda t: not t.is_fsm)
|
||||
super(Task, non_fsm_tasks)._compute_partner_id()
|
||||
|
||||
# For FSM tasks, only set partner_id if it's not already set
|
||||
fsm_tasks = self - non_fsm_tasks
|
||||
for task in fsm_tasks:
|
||||
if not task.partner_id:
|
||||
task.partner_id = self._get_default_partner_id(
|
||||
task.project_id, task.parent_id
|
||||
)
|
||||
|
|
|
|||
|
|
@ -125,18 +125,14 @@
|
|||
<th
|
||||
t-if="display_discount"
|
||||
class="text-right"
|
||||
groups="product.group_discount_per_so_line"
|
||||
groups="sale.group_discount_per_so_line"
|
||||
>
|
||||
<span>Disc.%</span>
|
||||
</th>
|
||||
<th class="text-right">
|
||||
<span
|
||||
groups="account.group_show_line_subtotals_tax_excluded"
|
||||
>
|
||||
<span>
|
||||
Amount</span>
|
||||
<span
|
||||
groups="account.group_show_line_subtotals_tax_included"
|
||||
>
|
||||
<span>
|
||||
Total Price</span>
|
||||
</th>
|
||||
</tr>
|
||||
|
|
@ -152,12 +148,10 @@
|
|||
<t
|
||||
t-set="current_subtotal"
|
||||
t-value="current_subtotal + line.delivered_price_subtotal"
|
||||
groups="account.group_show_line_subtotals_tax_excluded"
|
||||
/>
|
||||
<t
|
||||
t-set="current_total"
|
||||
t-value="current_subtotal + line.delivered_price_total"
|
||||
groups="account.group_show_line_subtotals_tax_included"
|
||||
/>
|
||||
<tr
|
||||
t-att-class="'bg-200 font-weight-bold o_line_section' if line.display_type == 'line_section' else 'font-italic o_line_note' if line.display_type == 'line_note' else ''"
|
||||
|
|
@ -186,7 +180,7 @@
|
|||
<td
|
||||
t-if="display_discount"
|
||||
class="text-right"
|
||||
groups="product.group_discount_per_so_line"
|
||||
groups="sale.group_discount_per_so_line"
|
||||
>
|
||||
<span t-field="line.discount" />
|
||||
</td>
|
||||
|
|
@ -350,7 +344,8 @@
|
|||
t-esc="doc.partner_id"
|
||||
t-options='{
|
||||
"widget": "contact",
|
||||
"fields": ["name", "address",]
|
||||
"fields": ["name", "address",],
|
||||
"lang": "fr_FR"
|
||||
}'
|
||||
/>
|
||||
</t>
|
||||
|
|
@ -361,11 +356,11 @@
|
|||
>
|
||||
<div t-if="doc.planned_date_begin"><h6>Planned start: </h6></div>
|
||||
<div class="mb-3">
|
||||
<div t-out="doc.planned_date_begin.strftime('%Y-%m-%d %H:%M')" />
|
||||
<div t-esc="context_timestamp(doc.planned_date_begin).strftime('%Y-%m-%d %H:%M')" />
|
||||
</div>
|
||||
<div t-if="doc.date_deadline"><h6>Planned end: </h6></div>
|
||||
<div class="mb-3">
|
||||
<div t-out="doc.date_deadline.strftime('%Y-%m-%d %H:%M')" />
|
||||
<div t-out="context_timestamp(doc.date_deadline).strftime('%Y-%m-%d %H:%M')" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -563,26 +558,24 @@
|
|||
<template id="work_order">
|
||||
<t t-call="web.html_container">
|
||||
<t t-foreach="docs" t-as="doc">
|
||||
<t t-set="doc" t-value="doc.root_ancestor" t-if="doc.parent_id" />
|
||||
<t t-set="doc" t-value="doc.root_ancestor.with_context(tz=doc.partner_id.tz)" t-if="doc.parent_id"/>
|
||||
<t t-set="lang" t-value="(doc.work_order_contacts and doc.work_order_contacts[0].lang) or (doc.partner_id and doc.partner_id.lang) or (doc.user_id and doc.user_id.lang) or user.lang"/>
|
||||
<t t-call="web.external_layout">
|
||||
<t
|
||||
t-call="bemade_fsm.work_order_page"
|
||||
t-lang="doc.partner_id.lang"
|
||||
/>
|
||||
<t t-call="bemade_fsm.work_order_page" t-lang="lang"/>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
<template
|
||||
id="worksheet_custom"
|
||||
inherit_id="industry_fsm_report.worksheet_custom"
|
||||
inherit_id="industry_fsm.worksheet_custom"
|
||||
priority="100"
|
||||
>
|
||||
<xpath
|
||||
expr="//t[@t-call='industry_fsm_report.worksheet_custom_page']"
|
||||
position="replace"
|
||||
>
|
||||
<div t-call="bemade_fsm.work_order_page" />
|
||||
<xpath expr="//t[@t-call='web.external_layout']" position="replace">
|
||||
<t t-set="lang" t-value="(doc.work_order_contacts and doc.work_order_contacts[0].lang) or (doc.partner_id and doc.partner_id.lang) or (doc.user_id and doc.user_id.lang) or user.lang"/>
|
||||
<t t-call="web.external_layout" t-lang="lang">
|
||||
<t t-call="bemade_fsm.work_order_page" t-lang="lang"/>
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from odoo import models
|
|||
|
||||
|
||||
class TaskCustomReport(models.AbstractModel):
|
||||
_inherit = "report.industry_fsm_report.worksheet_custom"
|
||||
_inherit = "report.industry_fsm.worksheet_custom"
|
||||
|
||||
def _get_report_values(self, docids, data=None):
|
||||
vals = super()._get_report_values(docids, data)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="industry_fsm_report.task_custom_report" model="ir.actions.report">
|
||||
<record id="industry_fsm.task_custom_report" model="ir.actions.report">
|
||||
<field name="print_report_name">'%s %s' % (
|
||||
object.planned_date_begin.strftime(
|
||||
'%Y-%m-%d') if object.planned_date_begin else time.strftime('%Y-%m-%d'),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<!-- Add a button to create a task from a template to task (project) list and kanban views -->
|
||||
<t t-name='project.KanbanView.buttons' t-inherit="web.KanbanView.buttons" t-inherit-mode="primary">
|
||||
<t t-name='project.KanbanView.buttons' t-inherit="web.KanbanView.Buttons" t-inherit-mode="primary">
|
||||
<xpath expr="//button[contains(@t-attf-class, 'o-kanban-button-new')]" position="after">
|
||||
<button title="Create Task from Template" t-if="!noCreate" type="button"
|
||||
t-attf-class="btn {{ btnClass }} o-kanban-button-new-from-template">
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
</button>
|
||||
</xpath>
|
||||
</t>
|
||||
<t t-name='project.ListView.buttons' t-inherit="web.ListView.buttons" t-inherit-mode="primary">
|
||||
<t t-name='project.ListView.buttons' t-inherit="web.ListView.Buttons" t-inherit-mode="primary">
|
||||
<xpath expr="//button[hasclass('o_list_button_add')]" position="after">
|
||||
<!-- Create is enabled in the parent template at this point; check is done prior -->
|
||||
<button type="button" class="btn ml-1 btn-primary o_list_button_add_from_template"
|
||||
|
|
|
|||
|
|
@ -42,8 +42,11 @@ class BemadeFSMBaseTest(TransactionCase):
|
|||
user_group_fsm_user = cls.env.ref("industry_fsm.group_fsm_user")
|
||||
user_group_sales_user = cls.env.ref("sales_team.group_sale_salesman")
|
||||
user_group_sales_manager = cls.env.ref("sales_team.group_sale_manager")
|
||||
user_group_delivery_address = cls.env.ref(
|
||||
"account.group_delivery_invoice_address"
|
||||
)
|
||||
user_product_customer = cls.env.ref(
|
||||
"customer_product_code.group_product_customer_code_user",
|
||||
"customer_product_code.group_product_customer_code_user", # pyright: ignore[reportGeneralTypeIssues]
|
||||
raise_if_not_found=False,
|
||||
)
|
||||
|
||||
|
|
@ -53,6 +56,7 @@ class BemadeFSMBaseTest(TransactionCase):
|
|||
user_group_fsm_user.id,
|
||||
user_group_sales_manager.id,
|
||||
user_group_sales_user.id,
|
||||
user_group_delivery_address.id,
|
||||
]
|
||||
if user_product_customer:
|
||||
group_ids.append(user_product_customer.id)
|
||||
|
|
@ -149,11 +153,15 @@ class BemadeFSMBaseTest(TransactionCase):
|
|||
"service_type": "timesheet",
|
||||
"project_id": (
|
||||
service_tracking in ("task_global_project", "project_only")
|
||||
and project
|
||||
and project.id
|
||||
or False
|
||||
),
|
||||
"project_template_id": (
|
||||
service_tracking == "task_in_project" and project.id or False
|
||||
service_tracking == "task_in_project"
|
||||
and project
|
||||
and project.id
|
||||
or False
|
||||
),
|
||||
"task_template_id": task_template and task_template.id or False,
|
||||
"service_policy": service_policy,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ class TestEquipment(BemadeFSMBaseTest):
|
|||
partner = self._generate_partner()
|
||||
partner_2 = self._generate_partner()
|
||||
equipment_1 = self._generate_equipment(partner=partner)
|
||||
equipment_2 = self._generate_equipment(partner_2)
|
||||
equipment_2 = self._generate_equipment(partner=partner_2)
|
||||
sale_order = self._generate_sale_order(partner=partner)
|
||||
product = self._generate_product()
|
||||
self.assertEqual(sale_order.valid_equipment_ids, equipment_1)
|
||||
|
|
|
|||
|
|
@ -142,3 +142,55 @@ class FSMVisitTest(BemadeFSMBaseTest):
|
|||
|
||||
supposed_name = "SVR12345-1 - Test Company - Test Label"
|
||||
self.assertEqual(task.name, supposed_name)
|
||||
|
||||
def test_subtasks_inherit_partner_from_parent_task(self):
|
||||
"""Test that subtasks of tasks created from FSM sales orders have their partner_id set correctly."""
|
||||
# Create a sale order with a shipping address different from the billing address
|
||||
partner = self._generate_partner(name="Customer")
|
||||
shipping_partner = self.env['res.partner'].create({
|
||||
'name': 'Shipping Address',
|
||||
'parent_id': partner.id,
|
||||
'type': 'delivery',
|
||||
})
|
||||
|
||||
# Create a sale order with the customer and shipping address
|
||||
so = self._generate_sale_order(partner=partner)
|
||||
so.partner_shipping_id = shipping_partner
|
||||
|
||||
# Create a visit
|
||||
visit = self._generate_visit(sale_order=so)
|
||||
|
||||
# Create a product with a task template that has subtasks
|
||||
parent_template = self._generate_task_template(
|
||||
structure=[1],
|
||||
names=["Parent Task", "Child Task"],
|
||||
)
|
||||
product = self._generate_product(task_template=parent_template)
|
||||
|
||||
# Add the product to the sale order
|
||||
sol = self._generate_sale_order_line(sale_order=so, product=product)
|
||||
|
||||
# Set the sequence to ensure proper ordering
|
||||
visit.so_section_id.sequence = 1
|
||||
sol.sequence = 2
|
||||
|
||||
# Confirm the sale order to create tasks
|
||||
so.action_confirm()
|
||||
|
||||
# Get the created tasks
|
||||
parent_task = sol.task_id
|
||||
self.assertTrue(parent_task, "Parent task should be created")
|
||||
|
||||
# Check that the parent task has a partner_id set
|
||||
self.assertTrue(parent_task.partner_id, "Parent task should have a partner set")
|
||||
|
||||
# Check that the subtask exists
|
||||
self.assertTrue(parent_task.child_ids, "Parent task should have subtasks")
|
||||
child_task = parent_task.child_ids[0]
|
||||
|
||||
# The key test: Check that the subtask has a partner_id set
|
||||
self.assertTrue(child_task.partner_id, "Subtask should have a partner_id set")
|
||||
|
||||
# Check that the subtask has the same partner as the parent task
|
||||
self.assertEqual(child_task.partner_id, parent_task.partner_id,
|
||||
"Subtask should have the same partner as its parent task")
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ class TestSalesOrder(BemadeFSMBaseTest):
|
|||
parent = self._generate_partner()
|
||||
child = self._generate_partner(parent=parent)
|
||||
for i in range(3):
|
||||
self._generate_equipment(child)
|
||||
self._generate_equipment(partner=child)
|
||||
|
||||
sale_order = self._generate_sale_order(partner=parent)
|
||||
|
||||
|
|
@ -139,11 +139,11 @@ class TestSalesOrder(BemadeFSMBaseTest):
|
|||
parent = self._generate_partner()
|
||||
child = self._generate_partner(parent=parent)
|
||||
for i in range(4):
|
||||
self._generate_equipment(child)
|
||||
self._generate_equipment(partner=child)
|
||||
|
||||
sale_order = self._generate_sale_order(partner=parent)
|
||||
|
||||
self.assertEqual(sale_order.default_equipment_ids, parent.owned_equipment_ids)
|
||||
self.assertEqual(sale_order.default_equipment_ids, self.env["fsm.equipment"])
|
||||
|
||||
def test_sale_order_resets_default_equipment_on_partner_change(self):
|
||||
partner_1 = self._generate_partner()
|
||||
|
|
@ -360,4 +360,115 @@ class TestSalesOrder(BemadeFSMBaseTest):
|
|||
}
|
||||
)
|
||||
for task in parent_task._get_all_subtasks() | parent_task:
|
||||
self.assertEqual(so.partner_shipping_id, task.partner_id)
|
||||
self.assertEqual(
|
||||
so.partner_shipping_id,
|
||||
task.partner_id,
|
||||
f"{task.name} has a different partner than the SO",
|
||||
)
|
||||
|
||||
def test_task_hierarchy_maintained_after_cancel_reconfirm(self):
|
||||
"""Test that task hierarchy and project assignments are maintained when canceling
|
||||
and reconfirming a sale order with a templated FSM product."""
|
||||
self.env.user.groups_id |= self.env.ref(
|
||||
"account.group_delivery_invoice_address"
|
||||
)
|
||||
# Create a task template with subtasks
|
||||
parent_template = self._generate_task_template(
|
||||
structure=[2], # Two subtasks
|
||||
names=["Main Service", "Subtask"],
|
||||
planned_hours=8,
|
||||
)
|
||||
|
||||
# Create FSM product with template
|
||||
product = self._generate_product(task_template=parent_template)
|
||||
|
||||
# Create and confirm sale order
|
||||
partner = self._generate_partner()
|
||||
partner_2 = self._generate_partner(parent=partner, company_type="person")
|
||||
self.assertEqual(partner_2.commercial_partner_id, partner)
|
||||
so = self._generate_sale_order(partner=partner)
|
||||
sol = self._generate_sale_order_line(so, product=product)
|
||||
so.action_confirm()
|
||||
|
||||
# Get initial tasks and verify setup
|
||||
main_task = sol.task_id
|
||||
self.assertTrue(main_task, "Main task should be created")
|
||||
self.assertTrue(main_task.project_id, "Main task should have a project")
|
||||
|
||||
subtasks = main_task.child_ids
|
||||
self.assertEqual(len(subtasks), 2, "Should have created 2 subtasks")
|
||||
self.assertEqual(so.tasks_count, 1, "Should have only 1 task on confirmation")
|
||||
|
||||
# Verify initial task hierarchy
|
||||
initial_project = main_task.project_id
|
||||
for subtask in subtasks:
|
||||
self.assertEqual(
|
||||
subtask.project_id,
|
||||
initial_project,
|
||||
"Subtask should have same project as main task",
|
||||
)
|
||||
self.assertFalse(
|
||||
subtask.sale_order_id, "Subtask should not be linked to sale order"
|
||||
)
|
||||
self.assertFalse(
|
||||
subtask.sale_line_id, "Subtask should not be linked to sale order line"
|
||||
)
|
||||
|
||||
# Store initial names for comparison
|
||||
initial_subtask_names = subtasks.mapped("name")
|
||||
|
||||
original_task_names = (main_task | main_task._get_all_subtasks()).mapped("name")
|
||||
# Cancel and reconfirm the sale order
|
||||
so.with_context(disable_cancel_warning=True).action_cancel()
|
||||
|
||||
so.action_draft()
|
||||
so.write({"partner_shipping_id": partner_2.id})
|
||||
# Get new tasks
|
||||
new_main_task = sol.task_id
|
||||
self.assertEqual(
|
||||
new_main_task, main_task, "New main task should be same as old"
|
||||
)
|
||||
|
||||
new_subtasks = new_main_task.child_ids
|
||||
new_subtasks._compute_sale_line()
|
||||
self.assertEqual(
|
||||
len(new_subtasks), 2, "Should still have 2 subtasks after reconfirmation"
|
||||
)
|
||||
self.assertFalse(
|
||||
new_subtasks.sale_line_id,
|
||||
"Subtasks should not be linked to Sale Order Line",
|
||||
)
|
||||
new_task_names = (new_main_task | new_main_task._get_all_subtasks()).mapped(
|
||||
"name"
|
||||
)
|
||||
self.assertEqual(
|
||||
new_task_names, original_task_names, "New task names should be the same"
|
||||
)
|
||||
|
||||
# Verify task hierarchy is maintained
|
||||
self.assertEqual(
|
||||
new_main_task.project_id,
|
||||
initial_project,
|
||||
"New main task should have same project",
|
||||
)
|
||||
|
||||
for subtask in new_subtasks:
|
||||
self.assertEqual(
|
||||
subtask.project_id,
|
||||
initial_project,
|
||||
"New subtask should maintain same project as main task",
|
||||
)
|
||||
self.assertFalse(
|
||||
subtask.sale_order_id, "New subtask should not be linked to sale order"
|
||||
)
|
||||
self.assertFalse(
|
||||
subtask.sale_line_id,
|
||||
"New subtask should not be linked to sale order line",
|
||||
)
|
||||
|
||||
# Verify subtask names are maintained
|
||||
self.assertEqual(
|
||||
sorted(new_subtasks.mapped("name")),
|
||||
sorted(initial_subtask_names),
|
||||
"Subtask names should be maintained after reconfirmation",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ class TestTaskReport(BemadeFSMBaseTest):
|
|||
service_product = self._generate_product()
|
||||
material_product = self._generate_product(
|
||||
name="Material Product",
|
||||
product_type="product",
|
||||
product_type="consu",
|
||||
service_tracking="no",
|
||||
)
|
||||
visit = self._generate_visit(sale_order=so)
|
||||
|
|
@ -23,12 +23,31 @@ class TestTaskReport(BemadeFSMBaseTest):
|
|||
so.action_confirm()
|
||||
task = visit.task_id
|
||||
|
||||
# Add timesheet entry to make the report renderable
|
||||
self.env["account.analytic.line"].create(
|
||||
{
|
||||
"name": "Test timesheet entry",
|
||||
"task_id": task.id,
|
||||
"project_id": task.project_id.id,
|
||||
"unit_amount": 2.0,
|
||||
"employee_id": self.env["hr.employee"]
|
||||
.create(
|
||||
{
|
||||
"name": "Test Employee",
|
||||
"user_id": self.env.user.id,
|
||||
}
|
||||
)
|
||||
.id,
|
||||
}
|
||||
)
|
||||
|
||||
html_content = (
|
||||
self.env["ir.actions.report"]
|
||||
._render(
|
||||
"industry_fsm_report.worksheet_custom",
|
||||
[task.id],
|
||||
)[0]
|
||||
"industry_fsm.worksheet_custom", [task.id]
|
||||
)[ # pyright: ignore[reportOptionalSubscript]
|
||||
0
|
||||
]
|
||||
.decode("utf-8")
|
||||
.split("\n")
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,36 +17,36 @@
|
|||
invisible="site_ids">
|
||||
<field
|
||||
name="site_contacts"
|
||||
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"
|
||||
context="{'list_view_ref': 'bemade_fsm.fsm_contacts_view_list'}"
|
||||
/>
|
||||
<field
|
||||
name="work_order_contacts"
|
||||
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"
|
||||
context="{'list_view_ref': 'bemade_fsm.fsm_contacts_view_list'}"
|
||||
/>
|
||||
</page>
|
||||
<field name="is_service_site" invisible="True"/>
|
||||
<page name="Service Sites" invisible="is_service_site">
|
||||
<group>
|
||||
<field name="site_ids">
|
||||
<tree editable="bottom">
|
||||
<list editable="bottom">
|
||||
<field name="name" widget="res_partner_many2one" />
|
||||
</tree>
|
||||
</list>
|
||||
</field>
|
||||
</group>
|
||||
</page>
|
||||
</page>
|
||||
</field>
|
||||
</record>
|
||||
<record id="fsm_contacts_view_tree" model="ir.ui.view">
|
||||
<field name="name">bemade_fsm.contacts.tree</field>
|
||||
<record id="fsm_contacts_view_list" model="ir.ui.view">
|
||||
<field name="name">bemade_fsm.contacts.list</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree editable="bottom">
|
||||
<list editable="bottom">
|
||||
<field name="name" />
|
||||
<field name="email" widget="email" />
|
||||
<field name="phone" widget="phone" />
|
||||
<field name="mobile" widget="phone" />
|
||||
</tree>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
<group name="fsm_visits" string="Service Visits">
|
||||
<field
|
||||
name="visit_ids"
|
||||
context="{'tree_view_ref': 'bemade_fsm.bemade_fsm_visit_tree'}"
|
||||
context="{'list_view_ref': 'bemade_fsm.bemade_fsm_visit_list'}"
|
||||
/>
|
||||
</group>
|
||||
<group name="field_service_info" string="Contacts and Equipment">
|
||||
|
|
@ -23,11 +23,11 @@
|
|||
/>
|
||||
<field
|
||||
name="site_contacts"
|
||||
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"
|
||||
context="{'list_view_ref': 'bemade_fsm.fsm_contacts_view_list'}"
|
||||
/>
|
||||
<field
|
||||
name="work_order_contacts"
|
||||
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"
|
||||
context="{'list_view_ref': 'bemade_fsm.fsm_contacts_view_list'}"
|
||||
/>
|
||||
|
||||
<field
|
||||
|
|
@ -39,7 +39,7 @@
|
|||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
<xpath expr="//tree//field[@name='name']" position="after">
|
||||
<xpath expr="//list//field[@name='name']" position="after">
|
||||
<field name="valid_equipment_ids" invisible="1" />
|
||||
<field
|
||||
name="equipment_ids"
|
||||
|
|
@ -49,16 +49,16 @@
|
|||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<record id="bemade_fsm_visit_tree" model="ir.ui.view">
|
||||
<field name="name">bemade_fsm.visit.tree</field>
|
||||
<record id="bemade_fsm_visit_list" model="ir.ui.view">
|
||||
<field name="name">bemade_fsm.visit.list</field>
|
||||
<field name="model">bemade_fsm.visit</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree editable="bottom">
|
||||
<list editable="bottom">
|
||||
<field name="label" />
|
||||
<field name="approx_date" />
|
||||
<field name="is_completed" widget="boolean" />
|
||||
<field name="is_invoiced" widget="boolean" />
|
||||
</tree>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
<field
|
||||
name="equipment_ids"
|
||||
domain="[('partner_id', '=', customer)]"
|
||||
context="{'tree_view_ref': 'fsm_equipment.equipment_view_tree'}"
|
||||
context="{'list_view_ref': 'fsm_equipment.equipment_view_list'}"
|
||||
/>
|
||||
<field name="tags" widget="many2many_tags" />
|
||||
<field name="company_id" />
|
||||
|
|
@ -40,7 +40,7 @@
|
|||
</page>
|
||||
<page name="subtasks_page" string="Subtasks">
|
||||
<field name="subtasks">
|
||||
<tree editable="bottom">
|
||||
<list editable="bottom">
|
||||
<field name="sequence" widget="handle" />
|
||||
<field name="name" />
|
||||
<field name="customer" />
|
||||
|
|
@ -55,7 +55,7 @@
|
|||
string="View Task"
|
||||
class="btn btn-link pull-right"
|
||||
/>
|
||||
</tree>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
|
|
@ -64,17 +64,17 @@
|
|||
</field>
|
||||
</record>
|
||||
|
||||
<record id="task_template_tree_view" model="ir.ui.view">
|
||||
<field name="name">project.task_template.tree</field>
|
||||
<record id="task_template_list_view" model="ir.ui.view">
|
||||
<field name="name">project.task_template.list</field>
|
||||
<field name="model">project.task.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<list>
|
||||
<field name="name" />
|
||||
<field name="assignees" widget="many2many_avatar_user" />
|
||||
<field name="project" />
|
||||
<field name="parent" />
|
||||
<field name="planned_hours" />
|
||||
</tree>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
|
@ -117,7 +117,7 @@
|
|||
<field name="name">Task Template</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">project.task.template</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="oe_view_nocontent_create">
|
||||
There are no task templates, click above to create one.
|
||||
|
|
|
|||
|
|
@ -21,15 +21,15 @@
|
|||
<field
|
||||
name="equipment_ids"
|
||||
domain="[('partner_id', '=', partner_id)]"
|
||||
context="{'tree_view_ref': 'fsm_equipment.equipment_view_tree'}"
|
||||
context="{'list_view_ref': 'fsm_equipment.equipment_view_list'}"
|
||||
/>
|
||||
<field
|
||||
name="site_contacts"
|
||||
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"
|
||||
context="{'list_view_ref': 'bemade_fsm.fsm_contacts_view_list'}"
|
||||
/>
|
||||
<field
|
||||
name="work_order_contacts"
|
||||
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"
|
||||
context="{'list_view_ref': 'bemade_fsm.fsm_contacts_view_list'}"
|
||||
/>
|
||||
</group>
|
||||
</page>
|
||||
|
|
@ -58,7 +58,7 @@
|
|||
<field name="name">bemade_fsm.project_task.form2</field>
|
||||
<field name="arch" type="xml">
|
||||
<xpath
|
||||
expr="//field[@name='child_ids']/tree//field[@name='name']"
|
||||
expr="//field[@name='child_ids']/list//field[@name='name']"
|
||||
position="after"
|
||||
>
|
||||
<field name="description" string="Description/Comments" />
|
||||
|
|
@ -77,9 +77,9 @@
|
|||
<field name="model">project.task</field>
|
||||
<field name="inherit_id" ref="industry_fsm.project_task_view_list_fsm" />
|
||||
<field name="arch" type="xml">
|
||||
<tree position="attributes">
|
||||
<list position="attributes">
|
||||
<attribute name="js_class">project_list</attribute>
|
||||
</tree>
|
||||
</list>
|
||||
<field name="name" position="before">
|
||||
<field name="work_order_number" optional="show" />
|
||||
</field>
|
||||
|
|
|
|||
|
|
@ -1,40 +0,0 @@
|
|||
#
|
||||
# Bemade Inc.
|
||||
#
|
||||
# Copyright (C) 2023-June Bemade Inc. (<https://www.bemade.org>).
|
||||
# Author: Marc Durepos (Contact : mdurepos@durpro.com)
|
||||
#
|
||||
# This program is under the terms of the Odoo Proprietary License v1.0 (OPL-1)
|
||||
# It is forbidden to publish, distribute, sublicense, or sell copies of the Software
|
||||
# or modified copies of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
# DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
{
|
||||
'name': 'Full Form from Dialog',
|
||||
'version': '17.0.1.0.0',
|
||||
'summary': 'Allows opening the full form view from the dialog (modal) view.',
|
||||
'description': 'Adds a button to open the full form view when viewing the form view for a record in a dialog.',
|
||||
'category': 'Technical',
|
||||
'author': 'Bemade Inc.',
|
||||
'website': 'http://www.bemade.org',
|
||||
'license': 'OPL-1',
|
||||
'depends': ['base'], # For testing, install contacts module as well.
|
||||
'data': [],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'bemade_full_formview_from_modal/static/src/**/*',
|
||||
],
|
||||
'web.assets_tests': [
|
||||
'bemade_full_formview_from_modal/static/tests/**/*',
|
||||
]
|
||||
},
|
||||
'installable': True,
|
||||
'auto_install': False
|
||||
}
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
# Migration vers Odoo 18.0 - bemade_full_formview_from_modal
|
||||
|
||||
## Description
|
||||
Module qui ajoute un bouton pour ouvrir la vue formulaire complète depuis une vue modale (dialog).
|
||||
|
||||
## Analyse Technique
|
||||
|
||||
### Fonctionnalité Native dans Odoo 18.0
|
||||
✅ La fonctionnalité existe nativement dans Odoo 18.0 !
|
||||
|
||||
Le composant `FormViewDialog` dans `web/static/src/views/view_dialogs/form_view_dialog.js` inclut déjà la méthode `onExpand()` qui fournit exactement la même fonctionnalité :
|
||||
```javascript
|
||||
async onExpand() {
|
||||
const beforeLeaveCallbacks = this.viewProps.__beforeLeave__.callbacks;
|
||||
const res = await Promise.all(beforeLeaveCallbacks.map((callback) => callback()));
|
||||
if (!res.includes(false)) {
|
||||
this.actionService.doAction({
|
||||
type: "ir.actions.act_window",
|
||||
res_model: this.props.resModel,
|
||||
res_id: this.currentResId,
|
||||
views: [[false, "form"]],
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Cette méthode :
|
||||
- Gère les callbacks avant de quitter la vue
|
||||
- Utilise le même service d'action
|
||||
- Préserve le contexte et l'ID de l'enregistrement
|
||||
- Ouvre la vue en mode plein écran
|
||||
|
||||
### Recommandation
|
||||
Ce module n'est plus nécessaire dans Odoo 18.0 car la fonctionnalité est maintenant disponible nativement.
|
||||
|
||||
## Plan de Migration
|
||||
|
||||
### Actions Requises
|
||||
1. **Désactivation du Module** :
|
||||
- [ ] Désactiver le module avant la migration vers Odoo 18.0
|
||||
- [ ] Vérifier qu'aucun autre module ne dépend de celui-ci
|
||||
- [ ] Informer les utilisateurs que la fonctionnalité est maintenant native
|
||||
|
||||
2. **Vérification** :
|
||||
- [ ] Tester la fonctionnalité native dans Odoo 18.0
|
||||
- [ ] Confirmer que tous les cas d'utilisation sont couverts
|
||||
- [ ] Documenter tout comportement différent pour les utilisateurs
|
||||
|
||||
## État de la Migration
|
||||
🟢 Pas de migration nécessaire - Utiliser la fonctionnalité native
|
||||
|
||||
## Notes Importantes
|
||||
- La fonctionnalité est maintenant intégrée nativement dans Odoo 18.0
|
||||
- Le comportement natif est identique à notre implémentation custom
|
||||
- Aucune personnalisation supplémentaire n'est nécessaire
|
||||
|
||||
## Prochaines Étapes
|
||||
1. Planifier la désactivation du module
|
||||
2. Documenter le changement pour les utilisateurs
|
||||
3. Retirer le module de la liste des dépendances des autres modules si nécessaire
|
||||
|
||||
## Notes de Version
|
||||
- Version originale: 17.0.1.0.0
|
||||
- Dernière analyse: 26/01/2025
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { X2ManyFieldDialog } from "@web/views/fields/relational_utils"
|
||||
import { FormViewDialog } from "@web/views/view_dialogs/form_view_dialog"
|
||||
|
||||
patch(X2ManyFieldDialog.prototype, {
|
||||
setup () {
|
||||
super.setup();
|
||||
this.action = useService('action')
|
||||
this.env.dialogData.onOpenButtonClicked = this.onOpenButtonClicked.bind(this);
|
||||
},
|
||||
onOpenButtonClicked: function () {
|
||||
this.action.doAction({
|
||||
type: "ir.actions.act_window",
|
||||
res_model: this.record.resModel,
|
||||
res_id: this.record.resId,
|
||||
views: [[false, "form"]],
|
||||
target: "current",
|
||||
context: this.props.context,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
patch(FormViewDialog.prototype, {
|
||||
setup () {
|
||||
super.setup();
|
||||
this.action = useService('action')
|
||||
this.onOpenButtonClicked = this.onOpenButtonClicked.bind(this);
|
||||
},
|
||||
onOpenButtonClicked() {
|
||||
this.action.doAction({
|
||||
type: "ir.actions.act_window",
|
||||
res_model: this.props.resModel,
|
||||
res_id: this.props.resId,
|
||||
views: [[false, "form"]],
|
||||
target: "current",
|
||||
context: this.props.context,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-inherit="web.X2ManyFieldDialog" t-inherit-mode="extension">
|
||||
<xpath expr="//t[@t-set-slot='footer']" position="inside">
|
||||
<button class="btn btn-primary" t-on-click="() => this.onOpenButtonClicked()">Open</button>
|
||||
</xpath>
|
||||
</t>
|
||||
<t t-inherit="web.FormViewDialog" t-inherit-mode="extension">
|
||||
<t t-set-slot="footer" position="inside">
|
||||
<button class="btn btn-secondary o_form_button_open" t-on-click="() => this.onOpenButtonClicked()">Open</button>
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { stepUtils } from "@web_tour/tour_service/tour_utils";
|
||||
|
||||
registry.category("web_tour.tours").add("full_formview_from_modal_tour", {
|
||||
test: true,
|
||||
url: '/web',
|
||||
steps: () => [stepUtils.showAppsMenuItem(),
|
||||
{
|
||||
content: 'Go to contacts',
|
||||
trigger: '.o_app[data-menu-xmlid="contacts.menu_contacts"]',
|
||||
},
|
||||
{
|
||||
content: 'Click search',
|
||||
trigger: '.o_searchview_input',
|
||||
},
|
||||
{
|
||||
content: 'insert text in the search bar',
|
||||
trigger: '.o_searchview_input',
|
||||
run: 'text Test parent',
|
||||
},
|
||||
{
|
||||
content: 'Validate search',
|
||||
trigger: '.o_searchview_autocomplete .o_menu_item:contains("Name")',
|
||||
},
|
||||
{
|
||||
content: 'Open the contact',
|
||||
trigger: '.o_kanban_record .o_kanban_record_title span:contains("Test parent")',
|
||||
},
|
||||
{
|
||||
content: 'Open the child',
|
||||
trigger: 'div[name="child_ids"] .o_kanban_record:first-child',
|
||||
},
|
||||
{
|
||||
"trigger": "button:contains('Open')",
|
||||
"content": "Click the open button on the modal",
|
||||
"run": "click",
|
||||
},
|
||||
{
|
||||
content: 'Make sure the form view opens to Test Child',
|
||||
trigger: 'div.o_last_breadcrumb_item span:contains("Test Child")',
|
||||
}
|
||||
]
|
||||
});
|
||||
|
|
@ -1 +0,0 @@
|
|||
from . import test_full_formview_from_modal
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
from odoo.tests import HttpCase, tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestFullFormviewFromModal(HttpCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.parent = cls.env['res.partner'].create({'name': 'Test parent', })
|
||||
cls.child = cls.env['res.partner'].create({'name': 'Test Child', 'parent_id': cls.parent.id})
|
||||
|
||||
def test_tour(self):
|
||||
self.start_tour("/web", 'full_formview_from_modal_tour', login="demo")
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
from . import models
|
||||
from . import wizard
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
#
|
||||
# Bemade Inc.
|
||||
#
|
||||
# Copyright (C) July 2023 Bemade Inc. (<https://www.bemade.org>).
|
||||
# Author: Marc Durepos (Contact : marc@bemade.org)
|
||||
#
|
||||
# This program is under the terms of the Odoo Proprietary License v1.0 (OPL-1)
|
||||
# It is forbidden to publish, distribute, sublicense, or sell copies of the Software
|
||||
# or modified copies of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
# DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
{
|
||||
'name': 'Bemade Addons from Git Repositories',
|
||||
'version': '17.0.1.0.0',
|
||||
'summary': 'A way to install addons from git repositories.',
|
||||
'description': """
|
||||
This module allows you to install addons from git repositories.
|
||||
|
||||
Configuration:
|
||||
|
||||
Set the directory where the repository will be cloned and the directory where the activated addons are located.
|
||||
|
||||
|
||||
Usage:
|
||||
|
||||
You can add a git repository in the Apps application and then enabled the addons from it.
|
||||
|
||||
In the Apps application, you will see a new option to add git repositories.
|
||||
This will allow you to select the repository and the branch you want to install.
|
||||
|
||||
You can then navigate to apps and in the menu, you will see a new option to enabled addons from git repositories.
|
||||
""",
|
||||
'category': 'Generic Modules/Others',
|
||||
'author': 'Bemade Inc.',
|
||||
'website': 'https://www.bemade.org',
|
||||
'license': 'OPL-1',
|
||||
'depends': [
|
||||
'base_import_module'
|
||||
],
|
||||
'data': [
|
||||
# 'data/default_directories_data.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'views/git_repos_views.xml',
|
||||
'views/res_settings_views.xml',
|
||||
'views/action_and_menu.xml',
|
||||
'wizard/directory_wizard_views.xml',
|
||||
'wizard/git_repos_wizard_views.xml',
|
||||
], 'demo': [],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'/bemade_git_repos_addons/static/src/views/*/*',
|
||||
],
|
||||
},
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<data noupdate="0">
|
||||
|
||||
<!-- Server Action to set the default values -->
|
||||
<record id="action_set_default_directories" model="ir.actions.server">
|
||||
<field name="name">Set Default Directories</field>
|
||||
<field name="model_id" ref="base.model_ir_config_parameter"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">
|
||||
env['ir.config_parameter'].sudo().set_param('bemade_git_repos_addons.clone_dir', '.repos')
|
||||
env['ir.config_parameter'].sudo().set_param('bemade_git_repos_addons.addons_dir', 'addons')
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Automated Action -->
|
||||
<record id="action_on_module_installation" model="base.automation">
|
||||
<field name="name">On Module Installation</field>
|
||||
<field name="model_id" ref="base.model_ir_module_module"/>
|
||||
<field name="trigger">on_create</field>
|
||||
<field name="filter_domain">[('name', '=', 'bemade_git_repos_addons'), ('state', '=', 'installed')]</field>
|
||||
<field name="action_server_id" ref="action_set_default_directories"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
from . import git_repos
|
||||
from . import git_branch
|
||||
from . import git_addons
|
||||
from . import res_settings
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
from odoo import api, fields, models
|
||||
import os
|
||||
|
||||
|
||||
class GitAddons(models.Model):
|
||||
_name = 'git.addons'
|
||||
_description = 'Git Addons'
|
||||
|
||||
name = fields.Char(string='Addon Name')
|
||||
branch_id = fields.Many2one('git.branch', string='Branch')
|
||||
|
||||
@api.model
|
||||
def get_addons(self):
|
||||
self.search([]).unlink() # Remove old records
|
||||
branches = self.env['git.branch'].search([])
|
||||
for branch in branches:
|
||||
repos = branch.repos
|
||||
addons_path = repos.addons_path
|
||||
if os.path.isdir(addons_path):
|
||||
addons = next(os.walk(addons_path))[1]
|
||||
for addon in addons:
|
||||
self.create({
|
||||
'name': addon,
|
||||
'branch_id': branch.id,
|
||||
})
|
||||
|
||||
def action_update_addons(self):
|
||||
self.get_addons()
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
from odoo import models, fields
|
||||
|
||||
|
||||
class GitBranch(models.Model):
|
||||
_name = 'git.branch'
|
||||
_description = 'Git Branch'
|
||||
|
||||
name = fields.Char(string='Branch Name', required=True)
|
||||
repo_id = fields.Many2one('git.repos', string='Repository')
|
||||
active = fields.Boolean(string='Active', default=False)
|
||||
branch_addons = fields.One2many(
|
||||
comodel_name='git.addons',
|
||||
inverse_name='branch_id',
|
||||
string='Addons',
|
||||
readonly=True)
|
||||
|
||||
|
||||
# If there are additional fields or relations you need, please define them here
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
from odoo import models, fields, api, exceptions
|
||||
|
||||
|
||||
class GitRepos(models.Model):
|
||||
_name = 'git.repos'
|
||||
_description = 'Git Repositories'
|
||||
|
||||
name = fields.Char(string='Name', required=True)
|
||||
url = fields.Char(string='URL')
|
||||
branches = fields.One2many('git.branch', 'repo_id', string='Branches', readonly=True)
|
||||
active_branch = fields.Many2one('git.branch', string='Active Branch')
|
||||
|
||||
@api.onchange('url')
|
||||
def _check_repo(self):
|
||||
self.branches = [(5, 0, 0)] # clear existing branches
|
||||
try:
|
||||
branches_list = self.get_branches(self.url) # a function you need to implement to get branches from git
|
||||
for branch in branches_list:
|
||||
self.env['git.branch'].create({
|
||||
'name': branch,
|
||||
'repo_id': self.id
|
||||
})
|
||||
except:
|
||||
return {
|
||||
'warning': {
|
||||
'title': "URL validation",
|
||||
'message': "The URL is not valid or the repository is not accessible.",
|
||||
},
|
||||
}
|
||||
|
||||
@api.model
|
||||
def get_branches(self, url):
|
||||
try:
|
||||
repo = Repo.clone_from(url, '/tmp/repo') # clone repository to a temp folder
|
||||
branches = [str(branch) for branch in repo.branches]
|
||||
return branches
|
||||
except Exception as e:
|
||||
# Log the error and return an empty list or handle the exception
|
||||
return []
|
||||
|
||||
@api.model
|
||||
def action_create_repo(self, vals_list=None):
|
||||
"""
|
||||
This method will be called from button that we have created using owl js
|
||||
"""
|
||||
if not vals_list:
|
||||
vals_list = [{'name': 'Test Repo', 'url': 'http://example.com', 'active_branch': 'master'}]
|
||||
repo = self.create(vals_list)
|
||||
return repo
|
||||
|
||||
@api.model
|
||||
def action_clone_repos(self):
|
||||
try:
|
||||
# Choose active branch
|
||||
repo = Repo('/tmp/repos/' + self.name)
|
||||
repo.git.checkout(self.active_branch.name)
|
||||
except Exception as e:
|
||||
# Log the error and handle the exception appropriately
|
||||
pass
|
||||
|
||||
@api.model
|
||||
def action_switch_branch(self, branch_id):
|
||||
try:
|
||||
# Choose a branch
|
||||
repo = Repo('/tmp/repos/' + self.name)
|
||||
repo.git.checkout(branch_id)
|
||||
# Update active_branch field
|
||||
self.active_branch = self.env['git.branch'].browse(branch_id)
|
||||
except Exception as e:
|
||||
# Log the error and handle the exception appropriately
|
||||
pass
|
||||
|
||||
@api.model
|
||||
def action_update_repos(self):
|
||||
self._check_repo()
|
||||
|
||||
@api.model
|
||||
def action_delete_repos(self):
|
||||
# Delete the physical directory '/tmp/repos/'+self.name
|
||||
# There is a lot of ways to do this. Here is one:
|
||||
import shutil
|
||||
shutil.rmtree('/tmp/repos/' + self.name)
|
||||
# Delete the DB record
|
||||
self.unlink()
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
_description = 'Directory Settings'
|
||||
|
||||
clone_dir = fields.Char(string='Clone Directory')
|
||||
addons_dir = fields.Char(string='Addons Directory')
|
||||
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
res = super().default_get(fields)
|
||||
|
||||
addons_dir = self.env['ir.config_parameter'].get_param('bemade_git_repos_addons.addons_dir')
|
||||
clone_dir = self.env['ir.config_parameter'].get_param('bemade_git_repos_addons.clone_dir')
|
||||
|
||||
if 'addons_dir' in fields and addons_dir:
|
||||
res['addons_dir'] = addons_dir
|
||||
if 'clone_dir' in fields and clone_dir:
|
||||
res['clone_dir'] = clone_dir
|
||||
return res
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
bemade_git_repos_addons.access_git_repos,bemade_git_repos.access_git_repos_user,bemade_git_repos_addons.model_git_repos,base.group_user,1,0,0,0
|
||||
bemade_git_repos_addons.access_git_repos_admin,bemade_git_repos.access_git_repos_admin,bemade_git_repos_addons.model_git_repos,base.group_system,1,1,1,1
|
||||
bemade_git_repos_addons.access_git_branch,bemade_git_repos.access_git_branch_user,bemade_git_repos_addons.model_git_branch,base.group_user,1,0,0,0
|
||||
bemade_git_repos_addons.access_git_branch_admin,bemade_git_repos.access_git_branch_admin,bemade_git_repos_addons.model_git_branch,base.group_system,1,1,1,1
|
||||
bemade_git_repos_addons.access_git_addons,bemade_git_repos.access_git_addons_user,bemade_git_repos_addons.model_git_addons,base.group_user,1,0,0,0
|
||||
bemade_git_repos_addons.access_git_addons_admin,bemade_git_repos.access_git_addons_admin,bemade_git_repos_addons.model_git_addons,base.group_system,1,1,1,1
|
||||
bemade_git_repos_addons.access_directory_wizard,bemade_git_repos.access_directory_wizard_user,bemade_git_repos_addons.model_directory_wizard,base.group_user,1,0,0,0
|
||||
bemade_git_repos_addons.access_directory_wizard_admin,bemade_git_repos.access_directory_wizard_admin,bemade_git_repos_addons.model_directory_wizard,base.group_system,1,1,1,1
|
||||
bemade_git_repos_addons.access_git_repos_wizard,bemade_git_repos.access_git_repos_wizard_user,bemade_git_repos_addons.model_git_repos_wizard,base.group_user,1,0,0,0
|
||||
bemade_git_repos_addons.access_git_repos_wizard_admin,bemade_git_repos.access_git_repos_wizard_admin,bemade_git_repos_addons.model_git_repos_wizard,base.group_system,1,1,1,1
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="bemade_git_repos_addons.ButtonCreateReposView.Buttons" t-inherit="web.ListView.Buttons" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//div[@t-if='props.showButtons']" position="after">
|
||||
<button type="button" t-on-click="onCreateRepos" class="btn btn-primary">
|
||||
Create
|
||||
</button>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { ListController } from "@web/views/list/list_controller";
|
||||
|
||||
export class ButtonCreateReposController extends ListController {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.orm = useService("orm");
|
||||
}
|
||||
|
||||
async onCreateRepos() {
|
||||
this.actionService.doAction({
|
||||
type: 'ir.actions.act_window',
|
||||
res_model: 'git.repos.wizard', // Replace 'your.wizard.model' with the model of your wizard
|
||||
views: [[false, 'form']],
|
||||
target: 'new',
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { listView } from "@web/views/list/list_view";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { ButtonCreateReposController as Controller } from './button_create_repos_controller';
|
||||
|
||||
export const ButtonCreateReposView = {
|
||||
...listView,
|
||||
Controller,
|
||||
buttonTemplate: 'bemade_git_repos_addons.ButtonCreateReposView.Buttons',
|
||||
};
|
||||
|
||||
registry.category("views").add("button_create_repos", ButtonCreateReposView);
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<data> <!-- Action to open the git_repos Tree View -->
|
||||
<record id="action_git_repos" model="ir.actions.act_window">
|
||||
<field name="name">Git Repositories</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">git.repos</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="domain">[]</field>
|
||||
</record>
|
||||
|
||||
<record id="action_git_addons" model="ir.actions.act_window">
|
||||
<field name="name">Git Addons</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">git.addons</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="domain">[]</field>
|
||||
</record>
|
||||
|
||||
<!-- Menu item to open the 'Git Addons' -->
|
||||
<menuitem id="menu_git_addons_main" name="Git Addons" parent="base.menu_apps" sequence="10" />
|
||||
|
||||
<!-- Sub-menu items -->
|
||||
<menuitem id="menu_git_repos" name="Git Repositories" parent="menu_git_addons_main" action="action_git_repos" sequence="1"/>
|
||||
<menuitem id="menu_git_addons" name="Git Addons" parent="menu_git_addons_main" action="action_git_addons" sequence="2"/>
|
||||
|
||||
|
||||
<!-- Menu item to open the git_repos Tree View -->
|
||||
<!-- <menuitem id="menu_git_repos" name="Git Repositories" parent="base.menu_settings" action="action_git_repos" sequence="1"/>-->
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- Tree View for the git_addons model -->
|
||||
<record id="view_git_addons_tree" model="ir.ui.view">
|
||||
<field name="name">git.addons.tree</field>
|
||||
<field name="model">git.addons</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="name"/>
|
||||
<field name="branch_id"/>
|
||||
<field name="active_branch"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Form View for the git_addons model -->
|
||||
<record id="view_git_addons_form" model="ir.ui.view">
|
||||
<field name="name">git.addons.form</field>
|
||||
<field name="model">git.addons</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="branch_id"/>
|
||||
<field name="active_branch"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<!-- Tree View for the git_repos model -->
|
||||
<record id="view_git_repos_tree" model="ir.ui.view">
|
||||
<field name="name">git.repos.tree</field>
|
||||
<field name="model">git.repos</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree create="false" js_class="button_create_repos">
|
||||
<field name="name"/>
|
||||
<field name="url"/>
|
||||
<field name="active_branch"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Form View for the git_repos model -->
|
||||
<record id="view_git_repos_form" model="ir.ui.view">
|
||||
<field name="name">git.repos.form</field>
|
||||
<field name="model">git.repos</field>
|
||||
<field name="arch" type="xml">
|
||||
<form create="false">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="url"/>
|
||||
<field name="active_branch"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="directory_wizard_action" model="ir.actions.act_window">
|
||||
<field name="name">Choose Directories</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">directory.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
<record id="view_res_config_settings" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
|
||||
<field name="priority" eval="9999"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@id='about']" position="before">
|
||||
|
||||
<block title="Git Addons" groups="base.group_no_one" name="git_addons">
|
||||
<setting id="git_directories" help="Settings for using git modules">
|
||||
<div string="Directories" data-string="Directories">
|
||||
<div class="row mt16">
|
||||
<label string="Clone Directory" for="clone_dir"/>
|
||||
<field name="clone_dir" readonly="1"/>
|
||||
</div>
|
||||
<div class="row mt16">
|
||||
<label string="Addons Directory" for="addons_dir"/>
|
||||
<field name="addons_dir" readonly="1"/>
|
||||
</div>
|
||||
<button name="%(directory_wizard_action)d" string="Choose Directories" type="action" class="oe_inline oe_stat_button"/>
|
||||
|
||||
</div>
|
||||
</setting>
|
||||
</block>
|
||||
|
||||
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
from . import directory_wizard
|
||||
from . import git_repos_wizard
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
from odoo import models, fields, api
|
||||
import os
|
||||
|
||||
|
||||
class DirectoryWizard(models.TransientModel):
|
||||
_name = 'directory.wizard'
|
||||
_description = 'Directory Wizard'
|
||||
|
||||
# Directories to exclude from the list, including hidden directories and directories that are not relevant
|
||||
EXCLUDED_DIRS = [
|
||||
'.idea',
|
||||
'.cache',
|
||||
'.config',
|
||||
'.git',
|
||||
'.local',
|
||||
'.odoo-deploy',
|
||||
'.ssh',
|
||||
'.testing',
|
||||
'conf',
|
||||
'design-themes',
|
||||
'enterprise',
|
||||
'filestore',
|
||||
'Notes',
|
||||
'odoo',
|
||||
'server',
|
||||
'themes',
|
||||
'tools',
|
||||
'venv',
|
||||
]
|
||||
|
||||
def _get_directory_list(self):
|
||||
# Fetch all directories in the current working directory
|
||||
directories = [(d, d) for d in os.listdir(os.getcwd())
|
||||
if os.path.isdir(d) and d not in self.EXCLUDED_DIRS]
|
||||
return directories
|
||||
|
||||
addons_dir = fields.Selection(_get_directory_list, string='Addons Directory')
|
||||
repos_dir = fields.Selection(_get_directory_list, string='Repos Directory')
|
||||
new_directory = fields.Char(string='New Directory')
|
||||
|
||||
def create_directory(self):
|
||||
for record in self:
|
||||
if not os.path.exists(record.new_directory):
|
||||
os.makedirs(record.new_directory)
|
||||
wizard = self.create({'new_directory': False}) # reset the value of new_directory
|
||||
return {
|
||||
'name': 'Directory Wizard',
|
||||
'res_model': self._name,
|
||||
'res_id': wizard.id,
|
||||
'views': [(False, 'form')],
|
||||
'type': 'ir.actions.act_window',
|
||||
'target': 'new',
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'warning': {
|
||||
'title': "Directory Exists",
|
||||
'message': "The directory already exists, please choose another name or directory.",
|
||||
}
|
||||
}
|
||||
|
||||
def apply_selections(self):
|
||||
IrConfigParameter = self.env['ir.config_parameter']
|
||||
|
||||
if self.addons_dir:
|
||||
IrConfigParameter.set_param('bemade_git_repos_addons.addons_dir', self.addons_dir)
|
||||
if self.repos_dir:
|
||||
IrConfigParameter.set_param('bemade_git_repos_addons.clone_dir', self.repos_dir)
|
||||
|
||||
return {}
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
res = super().default_get(fields)
|
||||
|
||||
addons_dir = self.env['ir.config_parameter'].get_param('bemade_git_repos_addons.addons_dir')
|
||||
repos_dir = self.env['ir.config_parameter'].get_param('bemade_git_repos_addons.clone_dir')
|
||||
|
||||
if 'addons_dir' in fields and addons_dir:
|
||||
res['addons_dir'] = addons_dir
|
||||
if 'repos_dir' in fields and repos_dir:
|
||||
res['repos_dir'] = repos_dir
|
||||
return res
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="directory_wizard_form_view" model="ir.ui.view">
|
||||
<field name="name">directory.wizard.form</field>
|
||||
<field name="model">directory.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Directory Selection">
|
||||
<sheet>
|
||||
<notebook>
|
||||
<page string="Set Directories">
|
||||
<group>
|
||||
<field name="addons_dir" string="Addons Directory"/>
|
||||
<field name="repos_dir" string="Repos Directory"/>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Create New irectory">
|
||||
<group>
|
||||
<field name="new_directory" string="Directory Name"/>
|
||||
<button name="create_directory" string="Create" type="object" class="btn-primary"/>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<footer>
|
||||
<button name="apply_selections" string="Apply" type="object" class="btn-success"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
from odoo import models, fields
|
||||
|
||||
|
||||
class GitReposWizard(models.TransientModel):
|
||||
_name = 'git.repos.wizard'
|
||||
_description = 'Git Repos Wizard'
|
||||
|
||||
url = fields.Char(string='Repository URL', required=True, help='URL of the git repository you want to clone')
|
||||
branch_id = fields.Many2one('git.branch', 'Active Branch')
|
||||
|
||||
def get_repo_branches(self):
|
||||
self.ensure_one()
|
||||
|
||||
# code for pulling branch list from git repo using `self.url`
|
||||
# here the git branch data should be transformed into {'name': 'branch_name'} form
|
||||
# branches_datum = [{'name': 'branch_name'}]
|
||||
#
|
||||
# # logic for checking if repo can be reach and have branches
|
||||
# if not branches_datum:
|
||||
# raise exceptions.ValidationError('The Repository URL is not accurate or The repository has no branches.')
|
||||
#
|
||||
# # create git.branch records or link with existing ones
|
||||
# for branch_data in branches_datum:
|
||||
|
||||
|
||||
# def action_confirm(self):
|
||||
# self.ensure_one()
|
||||
#
|
||||
# # Perform the git clone operation
|
||||
# # Be careful, errors should be managed
|
||||
# try:
|
||||
# git.Repo.clone_from(self.url, self.clone_dir)
|
||||
# except Exception as e:
|
||||
# raise Warning(str(e))
|
||||
#
|
||||
# return {'type': 'ir.actions.act_window_close'}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="view_git_repos_wizard_form" model="ir.ui.view">
|
||||
<field name="name">git.repos.wizard.form</field>
|
||||
<field name="model">git.repos.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Clone Repository">
|
||||
<group>
|
||||
<field name="url"/>
|
||||
<field name="branch_id"/>
|
||||
</group>
|
||||
<footer>
|
||||
<!-- <button name="action_confirm" string="Clone" type="object" class="btn-primary"/>-->
|
||||
<button string="Cancel" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -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
|
||||
|
|
@ -17,15 +17,15 @@
|
|||
# DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
{
|
||||
'name': 'Helpdesk One Ticket Per Email',
|
||||
'version': '17.0.1.0.0',
|
||||
'summary': 'Restrict ticket creation to a single ticket per email received.',
|
||||
'category': 'Helpdesk',
|
||||
'author': 'Bemade Inc.',
|
||||
'website': 'http://www.bemade.org',
|
||||
'license': 'OPL-1',
|
||||
'depends': ['mail'],
|
||||
'data': [],
|
||||
'installable': True,
|
||||
'auto_install': False
|
||||
"name": "Helpdesk One Ticket Per Email",
|
||||
"version": "18.0.1.0.0",
|
||||
"summary": "Restrict ticket creation to a single ticket per email received.",
|
||||
"category": "Helpdesk",
|
||||
"author": "Bemade Inc.",
|
||||
"website": "http://www.bemade.org",
|
||||
"license": "OPL-1",
|
||||
"depends": ["mail"],
|
||||
"data": [],
|
||||
"installable": True,
|
||||
"auto_install": False,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,72 +0,0 @@
|
|||
# Migration vers Odoo 18.0 - bemade_helpdesk_one_ticket_per_email
|
||||
|
||||
## Description
|
||||
Module qui restreint la création de tickets à un seul ticket par email reçu.
|
||||
|
||||
## Analyse Technique
|
||||
|
||||
### Fonctionnalités Actuelles
|
||||
1. **Extension du Routage des Messages**
|
||||
- Hérite de `mail.thread`
|
||||
- Surcharge de `_message_route_process`
|
||||
- Filtre les routes pour ne garder qu'une seule route helpdesk
|
||||
|
||||
2. **Comportement**
|
||||
- Détecte les routes liées au helpdesk (`helpdesk.ticket`, `helpdesk.team`)
|
||||
- Ne conserve que la première route helpdesk trouvée
|
||||
- Journalise les modifications de routage
|
||||
|
||||
### Changements dans Odoo 18.0
|
||||
|
||||
1. **Architecture Mail**
|
||||
- Le système de routage des emails reste similaire
|
||||
- La méthode `_message_route_process` existe toujours
|
||||
- Les modèles `helpdesk.ticket` et `helpdesk.team` sont inchangés
|
||||
|
||||
2. **Modifications Nécessaires**
|
||||
- [ ] Vérifier la compatibilité de la surcharge
|
||||
- [ ] Adapter le code pour la gestion des erreurs
|
||||
- [ ] Mettre à jour les dépendances
|
||||
|
||||
## Plan de Migration
|
||||
|
||||
### Phase 1 : Analyse et Préparation
|
||||
1. **Révision du Code**
|
||||
- [ ] Vérifier les changements dans `mail.thread`
|
||||
- [ ] Tester le comportement natif du routage
|
||||
- [ ] Identifier les potentiels conflits
|
||||
|
||||
2. **Tests**
|
||||
- [ ] Créer des cas de test pour les scénarios multiples
|
||||
- [ ] Documenter le comportement attendu
|
||||
- [ ] Préparer des emails de test
|
||||
|
||||
### Phase 2 : Migration
|
||||
1. **Mise à Jour du Code**
|
||||
- [ ] Adapter la surcharge de `_message_route_process`
|
||||
- [ ] Mettre à jour la gestion des erreurs
|
||||
- [ ] Vérifier la journalisation
|
||||
|
||||
2. **Tests et Validation**
|
||||
- [ ] Tester avec des emails simples
|
||||
- [ ] Tester avec des emails multiples
|
||||
- [ ] Vérifier la création unique des tickets
|
||||
|
||||
## État de la Migration
|
||||
En cours d'analyse - Migration simple requise
|
||||
|
||||
## Notes Importantes
|
||||
- La fonctionnalité reste pertinente dans Odoo 18.0
|
||||
- Le système de routage des emails est stable
|
||||
- La logique de base reste la même
|
||||
- Les tests seront cruciaux pour valider le comportement
|
||||
|
||||
## Prochaines Étapes
|
||||
1. Valider l'approche avec l'équipe
|
||||
2. Adapter le code pour Odoo 18.0
|
||||
3. Mettre à jour les tests
|
||||
4. Tester avec différents scénarios d'emails
|
||||
|
||||
## Notes de Version
|
||||
- Version originale: 17.0.1.0.0
|
||||
- Dernière analyse: 26/01/2025
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
from odoo import models, fields, api, _
|
||||
from odoo import models, api
|
||||
from odoo.exceptions import UserError
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MailThread(models.AbstractModel):
|
||||
_inherit = 'mail.thread'
|
||||
_inherit = "mail.thread"
|
||||
|
||||
@api.model
|
||||
def _message_route_process(self, message, message_dict, routes):
|
||||
|
|
@ -27,10 +28,14 @@ class MailThread(models.AbstractModel):
|
|||
"""
|
||||
try:
|
||||
# Filter routes to keep only those related to helpdesk models if they are present
|
||||
helpdesk_routes = [r for r in routes if r[0] in ('helpdesk.ticket', 'helpdesk.team')]
|
||||
helpdesk_routes = [
|
||||
r for r in routes if r[0] in ("helpdesk.ticket", "helpdesk.team")
|
||||
]
|
||||
|
||||
if helpdesk_routes:
|
||||
_logger.info("Messages contained helpdesk routes. Only the first one will be used.")
|
||||
_logger.info(
|
||||
"Messages contained helpdesk routes. Only the first one will be used."
|
||||
)
|
||||
# Retain only the first helpdesk route
|
||||
routes = [helpdesk_routes[0]]
|
||||
|
||||
|
|
@ -40,4 +45,6 @@ class MailThread(models.AbstractModel):
|
|||
except Exception as e:
|
||||
# Log the exception and raise it to ensure errors are traceable
|
||||
_logger.error(f"An error occurred in _message_route_process: {str(e)}")
|
||||
raise UserError("An unexpected error occurred while processing message routes. Please contact support.")
|
||||
raise UserError(
|
||||
"An unexpected error occurred while processing message routes. Please contact support."
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,23 +1,17 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
"name": "Hide Decimal on unit",
|
||||
"version": "17.0.0.1.1",
|
||||
"version": "18.0.0.1.1",
|
||||
"category": "Extra Tools",
|
||||
'summary': 'Hide decimal on Qty when there is no decimal',
|
||||
"summary": "Hide decimal on Qty when there is no decimal",
|
||||
"description": """
|
||||
Hide decimal on Qty when there is no decimal
|
||||
""",
|
||||
"author": "Bemade",
|
||||
'website': 'https://www.bemade.org',
|
||||
"depends": [
|
||||
'sale',
|
||||
'purchase'
|
||||
],
|
||||
"data": [
|
||||
'views/sale.xml',
|
||||
'views/purchase.xml'
|
||||
],
|
||||
"website": "https://www.bemade.org",
|
||||
"depends": ["sale", "purchase"],
|
||||
"data": ["views/sale.xml", "views/purchase.xml"],
|
||||
"auto_install": False,
|
||||
"installable": True,
|
||||
'license': 'OPL-1'
|
||||
"license": "OPL-1",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,72 +0,0 @@
|
|||
# Migration vers Odoo 18.0 - bemade_hide_decimal_on_unit
|
||||
|
||||
## Description
|
||||
Module qui cache les décimales sur les quantités lorsqu'elles sont entières dans les rapports de vente et d'achat.
|
||||
|
||||
## Analyse Technique
|
||||
|
||||
### Fonctionnalités Actuelles
|
||||
1. **Modification des Rapports**
|
||||
- Rapport de vente (`sale.report_saleorder_document`)
|
||||
- Rapport de devis d'achat (`purchase.report_purchasequotation_document`)
|
||||
- Rapport de commande d'achat (`purchase.report_purchaseorder_document`)
|
||||
|
||||
2. **Comportement**
|
||||
- Vérifie si la quantité est entière (`int(qty) == qty`)
|
||||
- Affiche sans décimale si entière (`'%.0f' % qty`)
|
||||
- Affiche avec décimales si non entière
|
||||
|
||||
### Changements dans Odoo 18.0
|
||||
|
||||
1. **Architecture des Rapports**
|
||||
- Les templates QWeb sont toujours utilisés
|
||||
- Les classes CSS `text-right` sont maintenant `text-end`
|
||||
- Les identifiants des rapports restent les mêmes
|
||||
|
||||
2. **Modifications Nécessaires**
|
||||
- [ ] Mettre à jour les classes CSS
|
||||
- [ ] Vérifier la compatibilité des expressions XPath
|
||||
- [ ] Valider les héritages de templates
|
||||
|
||||
## Plan de Migration
|
||||
|
||||
### Phase 1 : Analyse et Préparation
|
||||
1. **Révision du Code**
|
||||
- [ ] Vérifier les templates de base dans Odoo 18.0
|
||||
- [ ] Identifier les changements dans les classes CSS
|
||||
- [ ] Tester les expressions XPath
|
||||
|
||||
2. **Tests**
|
||||
- [ ] Créer des cas de test avec différentes quantités
|
||||
- [ ] Documenter le comportement attendu
|
||||
- [ ] Préparer des exemples de rapports
|
||||
|
||||
### Phase 2 : Migration
|
||||
1. **Mise à Jour des Vues**
|
||||
- [ ] Adapter les classes CSS (`text-right` → `text-end`)
|
||||
- [ ] Mettre à jour les expressions XPath si nécessaire
|
||||
- [ ] Vérifier les groupes de sécurité
|
||||
|
||||
2. **Tests et Validation**
|
||||
- [ ] Tester avec des quantités entières
|
||||
- [ ] Tester avec des quantités décimales
|
||||
- [ ] Vérifier l'affichage sur différents formats de rapport
|
||||
|
||||
## État de la Migration
|
||||
En cours d'analyse - Migration simple requise
|
||||
|
||||
## Notes Importantes
|
||||
- La fonctionnalité reste pertinente dans Odoo 18.0
|
||||
- Les changements sont principalement cosmétiques (CSS)
|
||||
- La logique de base reste la même
|
||||
- Les tests visuels seront importants
|
||||
|
||||
## 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 formats de rapport
|
||||
|
||||
## Notes de Version
|
||||
- Version originale: 17.0.0.1.1
|
||||
- Dernière analyse: 26/01/2025
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
#
|
||||
# Bemade Inc.
|
||||
#
|
||||
# Copyright (C) 2023-June Bemade Inc. (<https://www.bemade.org>).
|
||||
# Author: Marc Durepos (Contact : marc@bemade.org)
|
||||
#
|
||||
# This program is under the terms of the Odoo Proprietary License v1.0 (OPL-1)
|
||||
# It is forbidden to publish, distribute, sublicense, or sell copies of the Software
|
||||
# or modified copies of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
# DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
{
|
||||
'name': 'Quebec Payroll',
|
||||
'version': '17.0.1.0.0',
|
||||
'summary': 'Computations for Quebec Payslips',
|
||||
'category': 'Human Resources/Payroll',
|
||||
'author': 'Bemade Inc.',
|
||||
'website': 'http://www.bemade.org',
|
||||
'license': 'OPL-1',
|
||||
'depends': [
|
||||
'hr_payroll',
|
||||
'l10n_ca',
|
||||
],
|
||||
'data': [
|
||||
'data/hr_salary_rule_data.xml',
|
||||
],
|
||||
'assets': {},
|
||||
'installable': True,
|
||||
'auto_install': False
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="l10n_ca_input_bonus" model="hr.payslip.input.type">
|
||||
<field name="name">Bonus or other non-period payment</field>
|
||||
<field name="code">BONUS</field>
|
||||
<field name="country_id" ref="base.ca"/>
|
||||
<field name="struct_ids" eval="Command.link(ref('hr_payroll.default_structure'))"/>
|
||||
</record>
|
||||
<record id="l10n_ca_input_fed_f1" model="hr.payslip.input.type">
|
||||
<field name="name">Employee-requested deductions (Federal)</field>
|
||||
<field name="code">FED_F1</field>
|
||||
<field name="country_id" ref="base.ca"/>
|
||||
<field name="struct_ids" eval="Command.link(ref('hr_payroll.default_structure'))"/>
|
||||
</record>
|
||||
<record id="l10n_ca_input_fed_f2" model="hr.payslip.input.type">
|
||||
<field name="name">Court-ordered deductions (Federal)</field>
|
||||
<field name="code">FED_F2</field>
|
||||
<field name="country_id" ref="base.ca"/>
|
||||
<field name="struct_ids" eval="Command.link(ref('hr_payroll.default_structure'))"/>
|
||||
</record>
|
||||
<record id="l10n_ca_input_fed_U1" model="hr.payslip.input.type">
|
||||
<field name="name">Union or association dues for the period (Federal)</field>
|
||||
<field name="code">FED_U1</field>
|
||||
<field name="country_id" ref="base.ca"/>
|
||||
<field name="struct_ids" eval="Command.link(ref('hr_payroll.default_structure'))"/>
|
||||
</record>
|
||||
<record id="l10n_ca_input_fed_HD" model="hr.payslip.input.type">
|
||||
<field name="name">Allowance for residents of specified regions (Federal)</field>
|
||||
<field name="code">FED_HD</field>
|
||||
<field name="country_id" ref="base.ca"/>
|
||||
<field name="struct_ids" eval="Command.link(ref('hr_payroll.default_structure'))"/>
|
||||
</record>
|
||||
<record id="l10n_ca_input_fed_F" model="hr.payslip.input.type">
|
||||
<field name="name">Deduction for retirement plan contributions (Federal)</field>
|
||||
<field name="code">FED_F</field>
|
||||
<field name="country_id" ref="base.ca"/>
|
||||
<field name="struct_ids" eval="Command.link(ref('hr_payroll.default_structure'))"/>
|
||||
</record>
|
||||
<record id="l10n_ca_input_fed_TC" model="hr.payslip.input.type">
|
||||
<field name="name">Total Requested Amount on Federal form TD1</field>
|
||||
<field name="code">FED_TC</field>
|
||||
<field name="country_id" ref="base.ca"/>
|
||||
<field name="struct_ids" eval="Command.link(ref('hr_payroll.default_structure'))"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -1,146 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="l10n_ca_parameter_ARK" model="hr.rule.parameter">
|
||||
<field name="name">A, R, K constants for Federak Tax Calculation</field>
|
||||
<field name="code">ARK</field>
|
||||
</record>
|
||||
<record id="l10n_ca_parameter_value_ARK_2024" model="hr.rule.parameter.value">
|
||||
<field name="date_from">2024-01-01</field>
|
||||
<field name="rule_parameter_id" ref="bemade_l10n_ca_payroll.l10n_ca_parameter_ARK"/>
|
||||
<field name="parameter_value">
|
||||
[
|
||||
(0, 0.15, 0),
|
||||
(55867, 0.2050, 3073),
|
||||
(111733, 0.26, 9218),
|
||||
(173205, 0.29, 14414),
|
||||
(246752, 0.33, 24284),
|
||||
]
|
||||
</field>
|
||||
</record>
|
||||
<record id="l10n_ca_parameter_TC" model="hr.rule.parameter">
|
||||
<field name="name">Base amount for federal form TD1</field>
|
||||
<field name="code">FED_TC</field>
|
||||
</record>
|
||||
<record id="l10n_ca_parameter_value_TC_2024" model="hr.rule.parameter.value">
|
||||
<field name="date_from">2024-01-01</field>
|
||||
<field name="rule_parameter_id" ref="bemade_l10n_ca_payroll.l10n_ca_parameter_TC"/>
|
||||
<field name="parameter_value">15704</field>
|
||||
</record>
|
||||
|
||||
<record id="l10n_ca_fed_tax_on_payslip" model="hr.salary.rule">
|
||||
<field name="name">Income Tax - Federal</field>
|
||||
<field name="category_id" ref="hr_payroll.DED"/>
|
||||
<field name="sequence" eval="205"/>
|
||||
<field name="struct_id" ref="hr_payroll.default_structure"/>
|
||||
<field name="appears_on_payslip" eval="True"/>
|
||||
<field name="appears_on_employee_cost_dashboard" eval="False"/>
|
||||
<field name="appears_on_payroll_report" eval="True"/>
|
||||
<field name="condition_select">none</field>
|
||||
<field name="amount_select">code</field>
|
||||
<field name="amount_python_compute">
|
||||
"""<![CDATA[
|
||||
Source: https://www.canada.ca/fr/agence-revenu/services/formulaires-publications/retenues-paie/t4127-formules-calcul-retenues-paie/t4127-jan/t4127-jan-formules-calcul-informatise-retenues-paie.html#toc31
|
||||
|
||||
Pour le Québec, calcul en 4 étapes (étapes 1, 2, 3, et 6 du guide ci-dessus)
|
||||
Étape 1: Calculer "A" - le revenu imposable
|
||||
Étape 2: Calculer "T3" - l'impôt fédéral de base
|
||||
Étape 3: Calculer "T1" - l'impôt fédéral annuel à payer
|
||||
Étape 4: Calculer "T" - L'impôt à payer pour cette paye
|
||||
|
||||
|
||||
Étape 1
|
||||
|
||||
A = P x (I - F - F2 - F5A -U1) - HD - F1
|
||||
où
|
||||
P = Nombre de périodes de paie dans l'année
|
||||
I = Rémunération brute pour la période de paie, excluant les primes, augmentations salariales rétroactives ou autres paiements non-périodiques
|
||||
F = Retenues pour la période pour un REER, RPA, RPAC ou CR.
|
||||
F2 = saisie ordonnée par la cour (pension alimentaire, etc.)
|
||||
F5A = Déductions des cotisations supplémentaires au RRQ pour la période de paie
|
||||
U1 = Cotisations à un syndicat ou assoc. de fonctionnaires, pour la période de paie
|
||||
HD = Retenue annuelle accordée aux résidents d'une région visée par le règlement selon formulaire TD1
|
||||
F1 = Retenues annuelles (frais de garde d'enfants, pensions alimentaires, demandées par l'employé et autorisés par bureau svcs. fiscaux)
|
||||
PI = gains ouvrant droit à une pension pour la période de paie. Nous assumons ici que c'est égal à la rémunération brute.
|
||||
B = Prime brute, augmentation de salaire rétroactive, ou autres montants non périodiques
|
||||
|
||||
"""
|
||||
pay_periods_map = {
|
||||
'annually': 1,
|
||||
'semi-annually': 2,
|
||||
'quarterly': 4,
|
||||
'bi-monthly': 6,
|
||||
'monthly': 12,
|
||||
'semi-monthly': 24,
|
||||
'bi-weekly': 26,
|
||||
'weekly': 52,
|
||||
'daily': 365,
|
||||
}
|
||||
|
||||
P = payslip.struct_type_id.default_pay_periods_per_year
|
||||
I = categories.get("GROSS") - (inputs['BONUS'].amount if 'BONUS' in inputs else 0)
|
||||
F = inputs['FED_F'].amount if 'FED_F' in inputs else 0
|
||||
F2 = inputs['FED_F2'].amount if 'FED_F2' in inputs else 0
|
||||
C = categories.get("COTISATIONS_RRQ", 0)
|
||||
C2 = categories.get("COTISATIONS_RRQ_2", 0)
|
||||
F5Q = C * (0.01/0.0640) + C2
|
||||
PI = categories.get("GROSS")
|
||||
B = inputs['BONUS'].amount if 'BONUS' in inputs else 0
|
||||
F5A = F5Q * ((PI - B)/PI)
|
||||
U1 = inputs['FED_U1'].amount if 'FED_U1' in inputs else 0
|
||||
HD = inputs['FED_HD'].amount if 'FED_HD' in inputs else 0
|
||||
F1 = inputs['FED_F1'].amount if 'FED_F1' in inputs else 0
|
||||
|
||||
A = P * (I - F - F2 - F5A - U1) - HD - F1
|
||||
|
||||
"""
|
||||
Étape 2 - Calcul de l'impôt fédéral de base (T3)
|
||||
|
||||
T3 = (R x A) - K - K1 - K2Q - K3 - K4
|
||||
où
|
||||
|
||||
R = taux d'imposition fédéral qui s'applique au revenu imposable annuel A
|
||||
"""
|
||||
|
||||
# TODO: Get this into a configuration data structure
|
||||
ARK = payslip._rule_parameter('ARK')
|
||||
R, K = payslip._l10n_ca_compute_fed_tax_constants(A, ARK)
|
||||
TC = inputs['FED_TC'].amount if 'FED_TC' in inputs else payslip._rule_parameter('FED_TC_BASIC')
|
||||
K1 = 0.15 * TC
|
||||
PM = payslip._rule_parameter('RRQ_NO_MOIS_TOTAL')
|
||||
IE = A # Assume that insurables are the gross pay
|
||||
AE = categories.get("EI_CONTR")
|
||||
K2Q = ((0.15 * min(P * C * (0.0540/0.0640), 3217.50) * (PM/12)) + (0.15 * min(P * AE, 834.24) + (0.15 * min(P * IE * 0.00494, 464.36))
|
||||
K3 = inputs['FED_K3'].amount if 'FED_K3' in inputs else 0
|
||||
CCE = payslip._rule_parameter('FED_CCE') # 1 433 for 2024
|
||||
K4 = min(0.5 * A, CCE)
|
||||
|
||||
T3 = (R * A) - K - K1 - K2Q - K3 - K4
|
||||
|
||||
"""
|
||||
Étape 3 - Formule pour calculer l'impôt fédéral à payer (T1)
|
||||
|
||||
T1 = ((T3 - (P x LCF)) - (0.165 * T3)
|
||||
|
||||
"""
|
||||
|
||||
LCF = min(750, 0.15 * (inputs['DED_CAPITAL_PURCH'] if 'DED_CAPITAL_PURCH' in inputs else 0))
|
||||
T1 = (T3 - (P * LCF)) - (0.165 * T3)
|
||||
|
||||
"""
|
||||
Étape 6 - Formule pour calculer une estimation des retenus d'impôt fédéral pour la période de paie (T)
|
||||
|
||||
T = (T1 / P) / L
|
||||
|
||||
L = Retenues d'impôt additionnelles pour la période de paie, demandées par l'employé(e) sur TD1
|
||||
"""
|
||||
|
||||
L = inputs['FED_DEDUCT_REQUEST'].amount if 'FED_DEDUCT_REQUEST' in inputs else 0
|
||||
T = (T1 / P) / L
|
||||
|
||||
result = T
|
||||
]]>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -1 +0,0 @@
|
|||
from . import hr_payroll_structure_type
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class HrPayrollStructureType(models.Model):
|
||||
_inherit = 'hr.payroll.structure.type'
|
||||
|
||||
default_pay_periods_per_year = fields.Integer(
|
||||
compute="_compute_pay_periods_per_year",
|
||||
compute_sudo=True,
|
||||
)
|
||||
|
||||
@api.depends("default_schedule_pay")
|
||||
def _compute_pay_periods_per_year(self):
|
||||
pay_periods_map = {
|
||||
'annually': 1,
|
||||
'semi-annually': 2,
|
||||
'quarterly': 4,
|
||||
'bi-monthly': 6,
|
||||
'monthly': 12,
|
||||
'semi-monthly': 24,
|
||||
'bi-weekly': 26,
|
||||
'weekly': 52,
|
||||
'daily': 365,
|
||||
}
|
||||
for rec in self:
|
||||
rec.default_pay_periods_per_year = pay_periods_map.get(rec.default_schedule_pay, False)
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class Payslip(models.Model):
|
||||
_inherit = "hr.payslip"
|
||||
|
||||
def _l18n_ca_compute_fed_tax_constants(self, taxable_income: float, coefficients):
|
||||
"""
|
||||
Take a table of input coefficients in the form
|
||||
[
|
||||
(a, r, k),
|
||||
...
|
||||
] where a, r, and k are the threshold, tax rate and federal constants from government tables,
|
||||
and return the r and k applicable for the given annual taxable income.
|
||||
|
||||
:param taxable_income: annual taxable income
|
||||
:param coefficients: coefficients table to use (get it from rule parameters data, usually)
|
||||
:return: (r, k) values where r is the tax rate and k is the federal constant to use
|
||||
"""
|
||||
R = coefficients[0][1]
|
||||
K = coefficients[0][2]
|
||||
|
||||
# Get the rate and constant by income tier (stop once we reach a tier above the taxable income)
|
||||
for a, r, k in coefficients:
|
||||
if taxable_income < a:
|
||||
return R, K
|
||||
R = r
|
||||
K = k
|
||||
return R, K
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': 'Mailcow Integration',
|
||||
'version': '17.0.1.0.1',
|
||||
'category': 'Administration',
|
||||
'summary': 'Module for integrating Mailcow email server with Odoo.',
|
||||
'description': """
|
||||
Mailcow Integration
|
||||
|
||||
This module integrates the Mailcow email server with Odoo, providing a seamless email communication solution for your Odoo instance. It allows for syncing of mailboxes and email aliases from Mailcow to Odoo and vice versa.
|
||||
|
||||
Main Features:
|
||||
Synchronize Mailcow mailboxes with Odoo users.
|
||||
Synchronize Mailcow email aliases with Odoo.
|
||||
Configuration of Mailcow API credentials in Odoo settings.
|
||||
Automatically create and manage mailboxes and aliases in Mailcow when they are created in Odoo.
|
||||
""",
|
||||
'sequence': 10,
|
||||
'license': 'LGPL-3',
|
||||
'author': 'Bemade',
|
||||
'website': 'https://www.bemade.org',
|
||||
'depends': [
|
||||
'hr',
|
||||
'mail',
|
||||
'bemade_user_password_bundle'
|
||||
],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/mailcow_mailbox_views.xml',
|
||||
'views/mailcow_alias_views.xml',
|
||||
'views/mailcow_blacklist_views.xml',
|
||||
'views/res_users_views.xml',
|
||||
],
|
||||
"assets": {
|
||||
"web.assets_backend": [
|
||||
# BV: Commented out the following lines to avoid errors when installing the module.
|
||||
# "bemade_mailcow_integration/static/src/js/mailcow.js",
|
||||
# "bemade_mailcow_integration/static/src/xml/mailcow_templates.xml",
|
||||
],
|
||||
},
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'auto_install': False
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
from odoo import http
|
||||
from odoo.http import request
|
||||
|
||||
class EmailValidationController(http.Controller):
|
||||
|
||||
@http.route('/email_validation/<int:partner_id>/<token>', type='http', auth='public', website=True)
|
||||
def email_validation(self, partner_id, token):
|
||||
partner = request.env['res.partner'].sudo().browse(partner_id)
|
||||
if partner and partner.validation_token == token:
|
||||
partner.sudo().write({'email_validated': True})
|
||||
return "Email validated!"
|
||||
else:
|
||||
return "Invalid validation link."
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue