Remove modules with 17.0 versions
This commit is contained in:
parent
8424815d53
commit
ce69990df6
462 changed files with 0 additions and 19914 deletions
|
|
@ -1 +0,0 @@
|
|||
from . import models
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
{
|
||||
"name": "Account Credit Hold",
|
||||
"version": "17.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.",
|
||||
"maintainer": "Marc Durepos <marc@bemade.org>",
|
||||
"website": "http://www.bemade.org",
|
||||
"license": "LGPL-3",
|
||||
"depends": ["sale", "account_followup", "stock"],
|
||||
"data": [
|
||||
"views/account_followup_views.xml",
|
||||
"views/sale_order_views.xml",
|
||||
"views/res_partner_views.xml",
|
||||
"views/stock_picking_views.xml",
|
||||
],
|
||||
"demo": [],
|
||||
"installable": True,
|
||||
"auto_install": False,
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
from . import account_followup
|
||||
from . import res_partner
|
||||
from . import sale_order
|
||||
from . import stock_picking
|
||||
from . import account_followup_report
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
from odoo import fields, models, api
|
||||
|
||||
|
||||
class FollowupLine(models.Model):
|
||||
_inherit = 'account_followup.followup.line'
|
||||
|
||||
account_hold = fields.Boolean(string="Place on Credit Hold",
|
||||
help="Place clients on account hold, restricting confirmation of new orders.")
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
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)
|
||||
res.update({
|
||||
'credit_hold': followup_line.account_hold
|
||||
})
|
||||
return res
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
from odoo import fields, models, api, _
|
||||
from datetime import date
|
||||
|
||||
|
||||
class Partner(models.Model):
|
||||
_inherit = "res.partner"
|
||||
|
||||
postpone_hold_until = fields.Date(
|
||||
string="Postpone Hold",
|
||||
help="Grace period specific to this partner despite unpaid invoices.",
|
||||
tracking=True,
|
||||
)
|
||||
|
||||
hold_bg = fields.Boolean(
|
||||
string="Hold (technical)",
|
||||
compute="_compute_hold_bg",
|
||||
store=True,
|
||||
default=False,
|
||||
compute_sudo=True,
|
||||
tracking=True,
|
||||
)
|
||||
on_hold = fields.Boolean(
|
||||
string="Account on Hold",
|
||||
help="Client account is on hold for unpaid overdue invoices.",
|
||||
compute="_compute_on_hold",
|
||||
compute_sudo=True,
|
||||
)
|
||||
|
||||
@api.depends("postpone_hold_until", "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 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
|
||||
def _cleanup_expired_hold_postponements(self):
|
||||
expired_holds = self.search([("postpone_hold_until", "<=", date.today())])
|
||||
expired_holds.write({"postpone_hold_until": False})
|
||||
|
||||
def action_credit_hold(self):
|
||||
for rec in self:
|
||||
rec.hold_bg = True
|
||||
rec.message_post(body=_("Placed on credit hold."))
|
||||
|
||||
def action_lift_credit_hold(self):
|
||||
for rec in self:
|
||||
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.depends("followup_status", "followup_line_id")
|
||||
def _compute_hold_bg(self):
|
||||
first_followup_level = self._get_first_followup_level()
|
||||
for rec in self:
|
||||
prev_hold_bg = rec.hold_bg
|
||||
level = rec.followup_line_id
|
||||
if rec.followup_status == "no_action_needed" and not level:
|
||||
rec.hold_bg = False
|
||||
else:
|
||||
rec.hold_bg = prev_hold_bg
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
from odoo import fields, models, api, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_inherit = "sale.order"
|
||||
|
||||
client_on_hold = fields.Boolean(string='Client on Hold',
|
||||
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()
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
from odoo import fields, models, api
|
||||
|
||||
|
||||
class ModelName(models.Model):
|
||||
_inherit = "stock.picking"
|
||||
|
||||
client_on_hold = fields.Boolean(string='Client on Hold',
|
||||
help="Whether or not a client has been put on hold due to unpaid invoices.",
|
||||
related="partner_id.on_hold")
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
# Overview
|
||||
|
||||
This module adds the notion of placing clients on credit hold to the followup levels from the Odoo Enterprise
|
||||
account_followup module. It adds an option to followup levels to mark clients matching the followup criteria as on
|
||||
credit hold. This hold restricts the confirmation of new sales orders for these clients.
|
||||
|
||||
Accountant and admin users can set a date until which the account hold will be
|
||||
postponed on a specific partner's form view. This effectively gives clients an extra
|
||||
grace period, allowing orders to be confirmed until the period ends.
|
||||
|
||||
# Change Log
|
||||
|
||||
## 17.0.1.0.0 (2024-05-15)
|
||||
|
||||
Various modifications to adapt code to Odoo 17.0
|
||||
|
||||
## 15.0.2.0.0 (2023-05-04)
|
||||
|
||||
Complete remake of the module, making the "Credit Hold" an action that is either manually or
|
||||
automatically triggered from the Accounting > Followup Reports section or by setting the automatic application field
|
||||
on followup levels.
|
||||
|
||||
## 15.0.1.1.0 (2023-05-03)
|
||||
|
||||
Adds a ribbon to stock pickings for clients on hold, and therefore a dependency on stock.
|
||||
|
||||
## 15.0.1.0.2 (2023-05-03)
|
||||
|
||||
Fix to sale order view and sale order confirmation for clients not on hold.
|
||||
|
||||
## 15.0.1.0.1 (2023-05-03)
|
||||
|
||||
Fix clients on hold when status is "outstanding_invoices".
|
||||
|
||||
## 15.0.1.0.0 (2023-05-02) Initial Release
|
||||
|
||||
Initial release of the module, including a setting on follow-up levels to toggle placing on credit hold. Blocks
|
||||
the confirmation of sales orders for clients on credit hold. Red "Credit Hold" banner appears on sales orders and
|
||||
partner form view when a client is on credit hold. Credit hold can be postponed by setting the "Postpone Hold" field
|
||||
on the partner form view.
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="account_followup_followup_line_form_inherit" model="ir.ui.view">
|
||||
<field name="name">account_credit_hold.account_followup_line.form</field>
|
||||
<field name="model">account_followup.followup.line</field>
|
||||
<field
|
||||
name="inherit_id"
|
||||
ref="account_followup.view_account_followup_followup_line_form"
|
||||
/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='send_email']" position="before">
|
||||
<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>
|
||||
<field
|
||||
name="inherit_id"
|
||||
ref="account_followup.customer_statements_form_view"
|
||||
/>
|
||||
<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></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" />
|
||||
<field name="binding_model_id" ref="base.model_res_partner" />
|
||||
<field name="binding_view_types">list,form</field>
|
||||
<field name="state">code</field>
|
||||
<field name="code">records.action_credit_hold()</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="res_partner_form_inherit" model="ir.ui.view">
|
||||
<field name="name">account_credit_hold.res_partner.form</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='button_box']" position="before">
|
||||
<field invisible="True" name="hold_bg" />
|
||||
<field invisible="True" name="on_hold" />
|
||||
<widget
|
||||
bg_color="bg-danger"
|
||||
invisible="on_hold == False"
|
||||
name="web_ribbon"
|
||||
title="Credit Hold"
|
||||
/>
|
||||
</xpath></field>
|
||||
</record>
|
||||
<record id="view_partner_property_form_inherit" model="ir.ui.view">
|
||||
<field name="name">account_credit_hold.view_partner_property_form</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="account.view_partner_property_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@name='banks']" position="before">
|
||||
<group string="Credit Hold">
|
||||
<field
|
||||
groups="account.group_account_manager,account.group_account_user"
|
||||
name="postpone_hold_until"
|
||||
readonly="hold_bg == False and postpone_hold_until == False"
|
||||
/>
|
||||
</group>
|
||||
</xpath></field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="sale_order_form_inherit" model="ir.ui.view">
|
||||
<field name="name">account_credit_hold.sale_order.form</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="inherit_id" ref="sale.view_order_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='button_box']" position="before">
|
||||
<field invisible="True" name="client_on_hold" />
|
||||
<widget
|
||||
bg_color="bg-danger"
|
||||
invisible="client_on_hold == False"
|
||||
name="web_ribbon"
|
||||
title="Credit Hold"
|
||||
/>
|
||||
</xpath></field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="stock_picking_form_inherit" model="ir.ui.view">
|
||||
<field name="name">account_credit_hold.stock_picking.form</field>
|
||||
<field name="model">stock.picking</field>
|
||||
<field name="inherit_id" ref="stock.view_picking_form" />
|
||||
<field eval="8" name="priority" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='button_box']" position="before">
|
||||
<field invisible="True" name="client_on_hold" />
|
||||
<widget
|
||||
bg_color="bg-danger"
|
||||
invisible="client_on_hold == False"
|
||||
name="web_ribbon"
|
||||
title="Credit Hold"
|
||||
/>
|
||||
</xpath></field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
from . import models
|
||||
|
||||
|
||||
def post_init(env):
|
||||
env['account.report'].search([('name', 'ilike', 'Aged % - North America')]).unlink()
|
||||
new_receivable_report = env.ref('account_reports.aged_receivable_report').copy()
|
||||
new_payable_report = env.ref('account_reports.aged_payable_report').copy()
|
||||
new_receivable_report.line_ids.mapped('expression_ids').write({
|
||||
'formula': '_report_custom_engine_aged_receivable_na'
|
||||
})
|
||||
new_payable_report.line_ids.mapped('expression_ids').write({'formula': '_report_custom_engine_aged_payable_na'})
|
||||
new_receivable_report.write({
|
||||
'name': 'Aged Receivable - North America',
|
||||
'root_report_id': env.ref('account_reports.aged_receivable_report').id,
|
||||
'availability_condition': 'always',
|
||||
})
|
||||
new_payable_report.write({
|
||||
'name': 'Aged Payable - North America',
|
||||
'root_report_id': env.ref('account_reports.aged_payable_report').id,
|
||||
'availability_condition': 'always',
|
||||
})
|
||||
col_name_mapping = [
|
||||
('At Date', 'Overdue'),
|
||||
('1-30', '0-29'),
|
||||
('31-60', '30-59'),
|
||||
('61-90', '60-89'),
|
||||
('91-120', '90-119'),
|
||||
('Older', 'Later'),
|
||||
]
|
||||
for old_name, new_name in col_name_mapping:
|
||||
((new_payable_report | new_receivable_report)
|
||||
.mapped('column_ids')
|
||||
.filtered(lambda col: col.name == old_name)).write({'name': new_name})
|
||||
|
|
@ -1,32 +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': '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',
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
from . import aged_partner_balance
|
||||
|
|
@ -1,245 +0,0 @@
|
|||
from odoo import models, fields
|
||||
from itertools import chain
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
|
||||
class AgedPartnerBalanceCustomHandler(models.AbstractModel):
|
||||
_inherit = "account.aged.partner.balance.report.handler"
|
||||
|
||||
def _report_custom_engine_aged_receivable_na(
|
||||
self,
|
||||
expressions,
|
||||
options,
|
||||
date_scope,
|
||||
current_groupby,
|
||||
next_groupby,
|
||||
offset=0,
|
||||
limit=None,
|
||||
warnings=None
|
||||
):
|
||||
return self._aged_partner_report_custom_engine_common_na(
|
||||
options,
|
||||
'asset_receivable',
|
||||
current_groupby,
|
||||
next_groupby,
|
||||
offset=offset,
|
||||
limit=limit
|
||||
)
|
||||
|
||||
def _report_custom_engine_aged_payable_na(
|
||||
self,
|
||||
expressions,
|
||||
options,
|
||||
date_scope,
|
||||
current_groupby,
|
||||
next_groupby,
|
||||
offset=0,
|
||||
limit=None,
|
||||
warnings=None
|
||||
):
|
||||
return self._aged_partner_report_custom_engine_common_na(
|
||||
options,
|
||||
'liability_payable',
|
||||
current_groupby,
|
||||
next_groupby,
|
||||
offset=offset,
|
||||
limit=limit
|
||||
)
|
||||
|
||||
def _aged_partner_report_custom_engine_common_na(self, options, internal_type, current_groupby, next_groupby, offset=0, limit=None):
|
||||
report = self.env['account.report'].browse(options['report_id'])
|
||||
report._check_groupby_fields((next_groupby.split(',') if next_groupby else []) + ([current_groupby] if current_groupby else []))
|
||||
|
||||
def plus_days(date_obj, days):
|
||||
return fields.Date.to_string(date_obj + relativedelta(days=days))
|
||||
|
||||
def minus_days(date_obj, days):
|
||||
return fields.Date.to_string(date_obj - relativedelta(days=days))
|
||||
|
||||
date_to = fields.Date.from_string(options['date']['date_to'])
|
||||
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),
|
||||
]
|
||||
|
||||
def build_result_dict(report, query_res_lines):
|
||||
rslt = {f'period{i}': 0 for i in range(len(periods))}
|
||||
|
||||
for query_res in query_res_lines:
|
||||
for i in range(len(periods)):
|
||||
period_key = f'period{i}'
|
||||
rslt[period_key] += query_res[period_key]
|
||||
|
||||
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,
|
||||
'amount_currency': query_res['amount_currency'],
|
||||
'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,
|
||||
|
||||
# Needed by the custom_unfold_all_batch_data_generator, to speed-up unfold_all
|
||||
'partner_id': query_res['partner_id'][0] if query_res['partner_id'] else None,
|
||||
})
|
||||
else:
|
||||
rslt.update({
|
||||
'invoice_date': None,
|
||||
'due_date': None,
|
||||
'amount_currency': None,
|
||||
'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
|
||||
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)
|
||||
|
||||
# Build query
|
||||
tables, where_clause, where_params = report._query_get(options, 'strict_range', domain=[('account_id.account_type', '=', internal_type)])
|
||||
|
||||
currency_table = report._get_query_currency_table(options)
|
||||
always_present_groupby = "period_table.period_index, currency_table.rate, currency_table.precision"
|
||||
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}"
|
||||
else:
|
||||
select_from_groupby = ''
|
||||
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}
|
||||
"""
|
||||
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})
|
||||
|
||||
SELECT
|
||||
{select_from_groupby}
|
||||
%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 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 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}
|
||||
|
||||
FROM {tables}
|
||||
|
||||
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
|
||||
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
SUM(part.amount) AS amount,
|
||||
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
|
||||
GROUP BY part.debit_move_id
|
||||
) part_debit ON TRUE
|
||||
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
SUM(part.amount) AS amount,
|
||||
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
|
||||
GROUP BY part.credit_move_id
|
||||
) part_credit ON TRUE
|
||||
|
||||
JOIN period_table ON
|
||||
(
|
||||
period_table.date_start IS NULL
|
||||
OR COALESCE(account_move_line.date_maturity, account_move_line.date) >= DATE(period_table.date_start)
|
||||
)
|
||||
AND
|
||||
(
|
||||
period_table.date_stop IS NULL
|
||||
OR COALESCE(account_move_line.date_maturity, account_move_line.date) <= DATE(period_table.date_stop)
|
||||
)
|
||||
|
||||
WHERE {where_clause}
|
||||
|
||||
GROUP BY {groupby_clause}
|
||||
|
||||
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}
|
||||
"""
|
||||
|
||||
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)
|
||||
query_res_lines = self._cr.dictfetchall()
|
||||
|
||||
if not current_groupby:
|
||||
return build_result_dict(report, query_res_lines)
|
||||
else:
|
||||
rslt = []
|
||||
|
||||
all_res_per_grouping_key = {}
|
||||
for query_res in query_res_lines:
|
||||
grouping_key = query_res['grouping_key']
|
||||
all_res_per_grouping_key.setdefault(grouping_key, []).append(query_res)
|
||||
|
||||
for grouping_key, query_res_lines in all_res_per_grouping_key.items():
|
||||
rslt.append((grouping_key, build_result_dict(report, query_res_lines)))
|
||||
|
||||
return rslt
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from . import models
|
||||
|
||||
|
||||
|
||||
|
|
@ -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,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>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
from . import models
|
||||
from . import reports
|
||||
from . import wizard
|
||||
|
|
@ -1,70 +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": "Improved Field Service Management",
|
||||
"version": "17.0.0.4.2",
|
||||
"summary": (
|
||||
"Adds functionality necessary for managing field service operations at Durpro."
|
||||
),
|
||||
"category": "Services/Field Service",
|
||||
"author": "Bemade Inc.",
|
||||
"website": "http://www.bemade.org",
|
||||
"license": "LGPL-3",
|
||||
"depends": [
|
||||
"sale_stock",
|
||||
"sale_project",
|
||||
"account",
|
||||
"project_enterprise",
|
||||
"industry_fsm_stock",
|
||||
"industry_fsm_report",
|
||||
"industry_fsm_sale_report",
|
||||
"fsm_equipment",
|
||||
"bemade_partner_root_ancestor",
|
||||
"mail",
|
||||
],
|
||||
"data": [
|
||||
"data/fsm_data.xml",
|
||||
"views/task_template_views.xml",
|
||||
"security/ir.model.access.csv",
|
||||
"views/product_views.xml",
|
||||
"views/res_partner.xml",
|
||||
"views/menus.xml",
|
||||
"views/task_views.xml",
|
||||
"views/sale_order_views.xml",
|
||||
"reports/worksheet_custom_report_templates.xml",
|
||||
"reports/worksheet_custom_reports.xml",
|
||||
"wizard/new_task_from_template.xml",
|
||||
"wizard/res_config_settings.xml",
|
||||
],
|
||||
"assets": {
|
||||
"web.report_assets_common": ["bemade_fsm/static/src/scss/bemade_fsm.scss"],
|
||||
"web.assets_backend": [
|
||||
# BV: need to readd these files
|
||||
# 'bemade_fsm/static/src/js/kanban_view.js',
|
||||
# 'bemade_fsm/static/src/js/list_view.js',
|
||||
],
|
||||
"web.assets_qweb": [
|
||||
"bemade_fsm/static/src/xml/project_view_buttons.xml",
|
||||
],
|
||||
},
|
||||
"installable": True,
|
||||
"auto_install": False,
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<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>
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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%'"
|
||||
)
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
from . import task_template
|
||||
from . import product_template
|
||||
from . import sale_order_line
|
||||
from . import sale_order
|
||||
from . import task
|
||||
from . import res_partner
|
||||
from . import fsm_visit
|
||||
from . import res_company
|
||||
from . import project
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class FSMVisit(models.Model):
|
||||
_name = "bemade_fsm.visit"
|
||||
_description = "Represents a single visit by assigned service personnel."
|
||||
|
||||
label = fields.Text(
|
||||
string="Label",
|
||||
required=True,
|
||||
related="so_section_id.name",
|
||||
readonly=False,
|
||||
copy=True,
|
||||
)
|
||||
|
||||
approx_date = fields.Date(string="Approximate Date", copy=False)
|
||||
|
||||
so_section_id = fields.Many2one(
|
||||
comodel_name="sale.order.line",
|
||||
string="Sale Order Section",
|
||||
help=(
|
||||
"The section on the sale order that represents the labour and parts for"
|
||||
" this visit"
|
||||
),
|
||||
ondelete="cascade",
|
||||
)
|
||||
|
||||
sale_order_id = fields.Many2one(
|
||||
comodel_name="sale.order",
|
||||
string="Sales Order",
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
is_completed = fields.Boolean(
|
||||
string="Completed", related="so_section_id.is_fully_delivered"
|
||||
)
|
||||
|
||||
is_invoiced = fields.Boolean(
|
||||
string="Invoiced", related="so_section_id.is_fully_delivered_and_invoiced"
|
||||
)
|
||||
|
||||
summarized_equipment_ids = fields.Many2many(
|
||||
comodel_name="fsm.equipment",
|
||||
string="Equipment to Service",
|
||||
compute="_compute_summarized_equipment_ids",
|
||||
)
|
||||
|
||||
task_id = fields.Many2one(
|
||||
comodel_name="project.task",
|
||||
compute="_compute_task_id",
|
||||
string="Service Visit",
|
||||
)
|
||||
|
||||
task_ids = fields.One2many(comodel_name="project.task", inverse_name="visit_id")
|
||||
|
||||
visit_no = fields.Integer(
|
||||
compute="_compute_visit_no",
|
||||
)
|
||||
|
||||
@api.depends("task_ids")
|
||||
def _compute_task_id(self):
|
||||
for rec in self:
|
||||
rec.task_id = rec.task_ids and rec.task_ids[0]
|
||||
|
||||
@api.depends("so_section_id", "sale_order_id.summary_equipment_ids")
|
||||
def _compute_summarized_equipment_ids(self):
|
||||
for rec in self:
|
||||
lines = rec.so_section_id.get_section_line_ids()
|
||||
equipment_ids = []
|
||||
for line in lines:
|
||||
for equipment in line.equipment_ids:
|
||||
equipment_ids.append(equipment)
|
||||
rec.summarized_equipment_ids = equipment_ids
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
recs = super().create(vals_list)
|
||||
for i, rec in enumerate(recs.filtered(lambda visit: not visit.so_section_id)):
|
||||
rec.so_section_id = rec.env["sale.order.line"].create(
|
||||
{
|
||||
"order_id": rec.sale_order_id.id,
|
||||
"display_type": "line_section",
|
||||
"name": vals_list[i].get("label", False),
|
||||
}
|
||||
)
|
||||
return recs
|
||||
|
||||
@api.depends(
|
||||
"so_section_id",
|
||||
"sale_order_id",
|
||||
"sale_order_id.visit_ids",
|
||||
"sale_order_id.visit_ids.so_section_id",
|
||||
"sale_order_id.visit_ids.so_section_id.sequence",
|
||||
)
|
||||
def _compute_visit_no(self):
|
||||
for rec in self:
|
||||
ordered_visit_lines = self.sale_order_id.visit_ids.so_section_id.sorted(
|
||||
"sequence"
|
||||
)
|
||||
# Just a straight O(n) search here since n will always be relatively small
|
||||
for index, line in enumerate(ordered_visit_lines):
|
||||
if line == rec.so_section_id:
|
||||
rec.visit_no = index + 1
|
||||
return
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
from odoo import fields, models
|
||||
|
||||
|
||||
class ProductTemplate(models.Model):
|
||||
_inherit = "product.template"
|
||||
|
||||
task_template_id = fields.Many2one(
|
||||
comodel_name="project.task.template",
|
||||
string="Task Template",
|
||||
ondelete="restrict",
|
||||
)
|
||||
|
||||
is_field_service = fields.Boolean(
|
||||
string="Plan as field service",
|
||||
help=(
|
||||
"Products planned as field service will have travel time considered in"
|
||||
" planning."
|
||||
),
|
||||
)
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
from odoo import models
|
||||
|
||||
|
||||
class Project(models.Model):
|
||||
_inherit = "project.project"
|
||||
|
||||
def _fetch_sale_order_item_ids(
|
||||
self, domain_per_model=None, limit=None, offset=None
|
||||
):
|
||||
# Override to flush the ORM cache to the database prior to running the query
|
||||
# Temporary fix until Odoo fixes this method (PR #160067 submitted for this)
|
||||
self.env.flush_all()
|
||||
return super()._fetch_sale_order_item_ids(domain_per_model, limit, offset)
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
from odoo import models, fields
|
||||
|
||||
|
||||
class Company(models.Model):
|
||||
_inherit = "res.company"
|
||||
|
||||
split_time_from_materials_on_service_work_orders = fields.Boolean(default=False)
|
||||
create_default_fsm_visit = fields.Boolean(default=False)
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class Partner(models.Model):
|
||||
_inherit = "res.partner"
|
||||
|
||||
is_site_contact = fields.Boolean(
|
||||
string="Is a site contact",
|
||||
compute="_compute_is_site_contact",
|
||||
search="_search_is_site_contact",
|
||||
)
|
||||
|
||||
is_service_site = fields.Boolean(
|
||||
help="A partner is a service site if they have one or more equipments, "
|
||||
"site contacts or work order contacts."
|
||||
)
|
||||
|
||||
site_ids = fields.Many2many(
|
||||
string="Work Sites",
|
||||
comodel_name="res.partner",
|
||||
relation="res_partner_site_contact_rel",
|
||||
column1="site_contact_id",
|
||||
column2="site_id",
|
||||
tracking=True,
|
||||
domain=[("is_service_site", "=", True)],
|
||||
)
|
||||
|
||||
site_contacts = fields.Many2many(
|
||||
comodel_name="res.partner",
|
||||
relation="res_partner_site_contact_rel",
|
||||
column1="site_id",
|
||||
column2="site_contact_id",
|
||||
domain=[("is_service_site", "=", False)],
|
||||
tracking=True,
|
||||
)
|
||||
|
||||
work_order_contacts = fields.Many2many(
|
||||
string="Work Order Recipients",
|
||||
comodel_name="res.partner",
|
||||
relation="res_partner_work_order_contacts_rel",
|
||||
column1="res_partner_id",
|
||||
column2="work_order_contact_id",
|
||||
domain=[("is_service_site", "=", False)],
|
||||
tracking=True,
|
||||
)
|
||||
|
||||
@api.depends("site_ids")
|
||||
def _compute_is_site_contact(self):
|
||||
for rec in self:
|
||||
rec.is_site_contact = rec.site_ids
|
||||
|
||||
@api.model
|
||||
def _search_is_site_contact(self, operator, value):
|
||||
return [("site_ids", "!=", False)]
|
||||
|
||||
@api.depends("equipment_ids", "site_contacts", "work_order_contacts")
|
||||
def _compute_is_service_site(self):
|
||||
for rec in self:
|
||||
rec.is_service_site = (
|
||||
rec.equipment_ids or rec.site_contacts or rec.work_order_contacts
|
||||
)
|
||||
|
|
@ -1,163 +0,0 @@
|
|||
from odoo import fields, models, api, _, Command
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_inherit = "sale.order"
|
||||
|
||||
valid_equipment_ids = fields.One2many(
|
||||
comodel_name="fsm.equipment", related="partner_id.owned_equipment_ids"
|
||||
)
|
||||
|
||||
default_equipment_ids = fields.Many2many(
|
||||
comodel_name="fsm.equipment",
|
||||
string="Default Equipment to Service",
|
||||
help="The default equipment to service for new sale order lines.",
|
||||
compute="_compute_default_equipment",
|
||||
inverse="_inverse_default_equipment",
|
||||
store=True,
|
||||
)
|
||||
|
||||
summary_equipment_ids = fields.Many2many(
|
||||
comodel_name="fsm.equipment",
|
||||
string="Equipment Being Serviced",
|
||||
compute="_compute_summary_equipment_ids",
|
||||
)
|
||||
|
||||
site_contacts = fields.Many2many(
|
||||
comodel_name="res.partner",
|
||||
relation="sale_order_site_contacts_rel",
|
||||
compute="_compute_default_contacts",
|
||||
inverse="_inverse_default_contacts",
|
||||
store=True,
|
||||
)
|
||||
|
||||
work_order_contacts = fields.Many2many(
|
||||
comodel_name="res.partner",
|
||||
relation="sale_order_work_order_contacts_rel",
|
||||
compute="_compute_default_contacts",
|
||||
inverse="_inverse_default_contacts",
|
||||
string="Work Order Recipients",
|
||||
store=True,
|
||||
)
|
||||
|
||||
visit_ids = fields.One2many(
|
||||
comodel_name="bemade_fsm.visit", inverse_name="sale_order_id", readonly=False
|
||||
)
|
||||
|
||||
is_fsm = fields.Boolean(
|
||||
compute="_compute_is_fsm",
|
||||
string="Is FSM",
|
||||
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(
|
||||
lambda line: line.task_id == task_id
|
||||
or line == task_id.visit_id.so_section_id
|
||||
)
|
||||
visit_lines = linked_lines.filtered(lambda line: line.visit_id)
|
||||
for line in visit_lines:
|
||||
linked_lines |= line.get_section_line_ids()
|
||||
return linked_lines
|
||||
|
||||
@api.depends("order_line.equipment_ids")
|
||||
def _compute_summary_equipment_ids(self):
|
||||
for rec in self:
|
||||
rec.summary_equipment_ids = rec.order_line.mapped("equipment_ids")
|
||||
|
||||
@api.onchange("partner_shipping_id")
|
||||
def _onchange_partner_shipping_id(self):
|
||||
res = super()._onchange_partner_shipping_id()
|
||||
self._compute_default_equipment()
|
||||
self._compute_default_contacts()
|
||||
return res
|
||||
|
||||
@api.depends("partner_shipping_id")
|
||||
def _compute_default_contacts(self):
|
||||
for rec in self:
|
||||
rec.site_contacts = rec.partner_shipping_id.site_contacts
|
||||
rec.work_order_contacts = rec.partner_shipping_id.work_order_contacts
|
||||
|
||||
def _inverse_default_contacts(self):
|
||||
pass
|
||||
|
||||
@api.depends(
|
||||
"partner_id",
|
||||
"partner_shipping_id",
|
||||
"partner_shipping_id.equipment_ids",
|
||||
"partner_id.owned_equipment_ids",
|
||||
)
|
||||
def _compute_default_equipment(self):
|
||||
for rec in self:
|
||||
if rec.partner_shipping_id.equipment_ids:
|
||||
ids = rec.partner_shipping_id.equipment_ids
|
||||
else:
|
||||
ids = rec.partner_id.owned_equipment_ids
|
||||
rec.default_equipment_ids = ids if len(ids) < 4 else False
|
||||
|
||||
def _inverse_default_equipment(self):
|
||||
pass
|
||||
|
||||
def copy(self, default=None):
|
||||
rec = super().copy(default)
|
||||
rec.visit_ids = [Command.set(rec.order_line.visit_ids.ids)]
|
||||
return rec
|
||||
|
||||
def _create_default_visit(self):
|
||||
"""Called when an order is confirmed with lines that will create an FSM task,
|
||||
in order to make sure there is a visit line grouping all the service being done.
|
||||
"""
|
||||
self.ensure_one()
|
||||
visit = self.env["bemade_fsm.visit"].create(
|
||||
{
|
||||
"label": _("Service Visit"),
|
||||
"sale_order_id": self.id,
|
||||
}
|
||||
)
|
||||
# Make sure it goes to the top of the list
|
||||
visit.so_section_id.sequence = 0
|
||||
|
||||
def _create_or_organize_visits_if_needed(self):
|
||||
"""Adds a visit line to the top of the order if there are not already visit
|
||||
lines for an order with lines that will create an FSM task."""
|
||||
for order in self.filtered("company_id.create_default_fsm_visit"):
|
||||
if not order.visit_ids and order.is_fsm:
|
||||
order._create_default_visit()
|
||||
if order.is_fsm:
|
||||
# Make sure that all the lines producing FSM tasks are under a visit
|
||||
visit_line_ids = (
|
||||
order.mapped("visit_ids")
|
||||
.mapped("so_section_id")
|
||||
.mapped("section_line_ids")
|
||||
)
|
||||
if any(
|
||||
[
|
||||
True
|
||||
for line in order.order_line.filtered(
|
||||
lambda line: not line.display_type
|
||||
)
|
||||
if line not in visit_line_ids
|
||||
]
|
||||
):
|
||||
# If not, promote the first visit to the top of the order items list
|
||||
for line in order.order_line:
|
||||
line.sequence += 1
|
||||
order.mapped("visit_ids").mapped("so_section_id")[0].sequence = 0
|
||||
|
||||
@api.depends("order_line.is_fsm")
|
||||
def _compute_is_fsm(self):
|
||||
for rec in self:
|
||||
rec.is_fsm = any([line.is_fsm for line in rec.order_line])
|
||||
|
||||
def action_confirm(self):
|
||||
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:
|
||||
for rec in self:
|
||||
rec.tasks_ids.write({"partner_id": rec.partner_shipping_id.id})
|
||||
return res
|
||||
|
|
@ -1,294 +0,0 @@
|
|||
from odoo import fields, models, api, _, Command
|
||||
|
||||
|
||||
class SaleOrderLine(models.Model):
|
||||
_inherit = "sale.order.line"
|
||||
valid_equipment_ids = fields.One2many(
|
||||
comodel_name="fsm.equipment", related="order_id.valid_equipment_ids"
|
||||
)
|
||||
|
||||
visit_ids = fields.One2many(
|
||||
comodel_name="bemade_fsm.visit",
|
||||
inverse_name="so_section_id",
|
||||
string="Visits",
|
||||
copy=True,
|
||||
)
|
||||
|
||||
visit_id = fields.Many2one(
|
||||
comodel_name="bemade_fsm.visit",
|
||||
compute="_compute_visit_id",
|
||||
string="Visit",
|
||||
ondelete="cascade",
|
||||
store=True,
|
||||
)
|
||||
|
||||
is_fully_delivered = fields.Boolean(
|
||||
string="Fully Delivered",
|
||||
compute="_compute_is_fully_delivered",
|
||||
help=(
|
||||
"Indicates whether a line or all the lines in a section have been entirely"
|
||||
" delivered."
|
||||
),
|
||||
)
|
||||
|
||||
is_fully_delivered_and_invoiced = fields.Boolean(
|
||||
string="Fully Invoiced",
|
||||
compute="_compute_is_fully_invoiced",
|
||||
help=(
|
||||
"Indicates whether a line or all the lines in a section have been entirely"
|
||||
" delivered and invoiced."
|
||||
),
|
||||
)
|
||||
|
||||
equipment_ids = fields.Many2many(
|
||||
string="Equipment to Service",
|
||||
comodel_name="fsm.equipment",
|
||||
relation="bemade_fsm_equipment_sale_order_line_rel",
|
||||
column1="sale_order_line_id",
|
||||
column2="equipment_id",
|
||||
)
|
||||
|
||||
is_field_service = fields.Boolean(compute="_compute_is_field_service", store=True)
|
||||
|
||||
is_fsm = fields.Boolean(
|
||||
string="Is FSM",
|
||||
compute="_compute_is_fsm",
|
||||
store=True,
|
||||
)
|
||||
|
||||
section_line_ids = fields.One2many(
|
||||
comodel_name="sale.order.line",
|
||||
compute="_compute_section_line_ids",
|
||||
)
|
||||
|
||||
@api.depends("visit_ids")
|
||||
def _compute_visit_id(self):
|
||||
for rec in self:
|
||||
rec.visit_id = rec.visit_ids and rec.visit_ids[0]
|
||||
|
||||
@api.depends("product_id")
|
||||
def _compute_is_field_service(self):
|
||||
for rec in self:
|
||||
rec.is_field_service = rec.product_id.is_field_service
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals):
|
||||
recs = super().create(vals)
|
||||
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
|
||||
return recs
|
||||
|
||||
def copy_data(self, default=None):
|
||||
if default is None:
|
||||
default = {}
|
||||
if "visit_ids" not in default:
|
||||
default["visit_ids"] = [
|
||||
(0, 0, visit.copy_data()[0]) for visit in self.visit_ids
|
||||
]
|
||||
return super().copy_data(default)
|
||||
|
||||
def _timesheet_create_task(self, project):
|
||||
"""Generate task for the given so line, and link it.
|
||||
:param project: record of project.project in which the task should be created
|
||||
:return task: record of the created task
|
||||
|
||||
Override to add the logic needed to implement task templates and equipment linkages.
|
||||
"""
|
||||
|
||||
def _create_task_from_template(project, template, parent):
|
||||
"""Recursively generates the task and any subtasks from a project.task.template.
|
||||
|
||||
:param project: project.project record to set on the task's project_id field.
|
||||
:param template: project.task.template to use to create the task.
|
||||
:param parent: project.task to set as the parent to this task.
|
||||
"""
|
||||
values = _timesheet_create_task_prepare_values_from_template(
|
||||
project, template, parent
|
||||
)
|
||||
task = self.env["project.task"].sudo().create(values)
|
||||
subtasks = []
|
||||
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,
|
||||
}
|
||||
)
|
||||
return task
|
||||
|
||||
def _timesheet_create_task_prepare_values_from_template(
|
||||
project, template, parent
|
||||
):
|
||||
"""Copies the values from a project.task.template over to the set of values used to create a project.task.
|
||||
|
||||
:param project: project.project record to set on the task's project_id field.
|
||||
Pass the project.project model or an empty recordset to leave task project_id blank.
|
||||
DO NOT pass False or None as this will cause an error in _timesheet_create_task_prepare_values(project).
|
||||
:param template: project.task.template to use to create the task.
|
||||
:param parent: project.task to set as the parent to this task.
|
||||
"""
|
||||
vals = self._timesheet_create_task_prepare_values(project)
|
||||
vals["name"] = template.name
|
||||
vals["description"] = (
|
||||
template.description or "" if parent else vals["description"]
|
||||
)
|
||||
vals["parent_id"] = parent and parent.id
|
||||
vals["user_ids"] = template.assignees.ids
|
||||
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
|
||||
if template.equipment_ids:
|
||||
vals["equipment_ids"] = template.equipment_ids.ids
|
||||
return vals
|
||||
|
||||
tmpl = self.product_id.task_template_id
|
||||
if not tmpl:
|
||||
task = super()._timesheet_create_task(project)
|
||||
else:
|
||||
task = _create_task_from_template(project, tmpl, None)
|
||||
self.write({"task_id": task.id})
|
||||
# post message on task
|
||||
task_msg = _(
|
||||
"This task has been created from: <a href=# data-oe-model=sale.order"
|
||||
" data-oe-id=%(so_id)d>%(so_name)s</a> (%(product_name)s)"
|
||||
) % {
|
||||
"so_id": self.order_id.id,
|
||||
"so_name": self.order_id.name,
|
||||
"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
|
||||
|
||||
def _timesheet_service_generation(self):
|
||||
super()._timesheet_service_generation()
|
||||
visit_lines = self.filtered(lambda line: line.visit_id)
|
||||
for index, line in enumerate(visit_lines):
|
||||
task_ids = line.get_section_line_ids().mapped("task_id")
|
||||
if not task_ids:
|
||||
continue
|
||||
if len(set([task.project_id for task in task_ids])) > 1:
|
||||
# Can't group up the tasks if they're part of different projects
|
||||
return
|
||||
project_id = task_ids[0].project_id
|
||||
line.visit_id.task_id = line._generate_task_for_visit_line(
|
||||
project_id, index + 1, sum(task_ids.mapped("allocated_hours"))
|
||||
)
|
||||
task_ids.write({"parent_id": line.visit_id.task_id.id})
|
||||
(self.mapped("task_id") | self.visit_ids.task_id).filtered(
|
||||
"is_fsm"
|
||||
).synchronize_name_fsm()
|
||||
|
||||
def _generate_task_for_visit_line(
|
||||
self, project, visit_no: int, allocated_hours: int
|
||||
):
|
||||
self.ensure_one()
|
||||
|
||||
allocated_hours = sum(
|
||||
self.get_section_line_ids().task_id.mapped("allocated_hours")
|
||||
)
|
||||
task = self.env["project.task"].create(
|
||||
{
|
||||
"name": (
|
||||
f"{self.order_id.name} - "
|
||||
+ _("Visit")
|
||||
+ f" {visit_no} - {self.name}"
|
||||
),
|
||||
"project_id": project.id,
|
||||
"equipment_ids": (
|
||||
self.get_section_line_ids().mapped("equipment_ids").ids
|
||||
),
|
||||
"sale_order_id": self.order_id.id,
|
||||
"partner_id": self.order_id.partner_shipping_id.id,
|
||||
"visit_id": self.visit_id.id,
|
||||
"allocated_hours": allocated_hours,
|
||||
"date_deadline": self.visit_id.approx_date,
|
||||
"user_ids": False, # Force to empty or it uses the current user
|
||||
}
|
||||
)
|
||||
return task
|
||||
|
||||
@api.depends(
|
||||
"order_id.order_line",
|
||||
"display_type",
|
||||
"qty_to_deliver",
|
||||
"order_id.order_line.qty_to_deliver",
|
||||
"order_id.order_line.display_type",
|
||||
)
|
||||
def _compute_is_fully_delivered(self):
|
||||
for rec in self:
|
||||
rec.is_fully_delivered = rec._iterate_items_compute_bool(
|
||||
lambda line: line.qty_to_deliver == 0
|
||||
)
|
||||
|
||||
@api.depends("is_fully_delivered")
|
||||
def _compute_is_fully_invoiced(self):
|
||||
for rec in self:
|
||||
if not rec.is_fully_delivered:
|
||||
rec.is_fully_delivered_and_invoiced = False
|
||||
return
|
||||
rec.is_fully_delivered_and_invoiced = rec._iterate_items_compute_bool(
|
||||
lambda line: line.qty_to_invoice == 0
|
||||
)
|
||||
|
||||
def get_section_line_ids(self):
|
||||
"""Return a recordset of sale.order.line records that are in this sale order section."""
|
||||
self.ensure_one()
|
||||
assert (
|
||||
self.display_type == "line_section"
|
||||
), "Cannot get section lines for a non-section."
|
||||
found = False
|
||||
lines = []
|
||||
for line in self.order_id.order_line.sorted(lambda line: line.sequence):
|
||||
if line == self:
|
||||
found = True
|
||||
continue
|
||||
if not found:
|
||||
continue
|
||||
if line.display_type == "line_section": # Stop when we hit the next section
|
||||
break
|
||||
else:
|
||||
lines.append(line)
|
||||
return self.env["sale.order.line"].union(*lines)
|
||||
|
||||
@api.depends("display_type", "order_id.order_line")
|
||||
def _compute_section_line_ids(self):
|
||||
for rec in self:
|
||||
if rec.display_type == "line_section":
|
||||
rec.section_line_ids = [Command.set(rec.get_section_line_ids().ids)]
|
||||
else:
|
||||
rec.section_line_ids = False
|
||||
|
||||
def _iterate_items_compute_bool(self, single_line_func):
|
||||
if not self.display_type:
|
||||
return single_line_func(self)
|
||||
elif self.display_type == "line_note":
|
||||
return True
|
||||
else:
|
||||
for line in self.order_id.order_line:
|
||||
found = False
|
||||
if line == self:
|
||||
found = True
|
||||
if not found:
|
||||
continue
|
||||
if found and line.display_type == "line_section":
|
||||
return True
|
||||
val = single_line_func(self)
|
||||
if not val:
|
||||
return val
|
||||
return True
|
||||
|
||||
@api.depends("product_id.detailed_type", "product_id.service_tracking")
|
||||
def _compute_is_fsm(self):
|
||||
for rec in self:
|
||||
rec.is_fsm = (
|
||||
rec.product_id.detailed_type == "service"
|
||||
and rec.product_id.service_tracking == "task_global_project"
|
||||
)
|
||||
|
|
@ -1,222 +0,0 @@
|
|||
from odoo import fields, models, api, Command
|
||||
from odoo.addons.project.models.project_task import CLOSED_STATES
|
||||
import re
|
||||
|
||||
|
||||
class Task(models.Model):
|
||||
_inherit = "project.task"
|
||||
|
||||
work_order_contacts = fields.Many2many(
|
||||
comodel_name="res.partner",
|
||||
relation="task_work_order_contact_rel",
|
||||
column1="task_id",
|
||||
column2="res_partner_id",
|
||||
)
|
||||
|
||||
site_contacts = fields.Many2many(
|
||||
comodel_name="res.partner",
|
||||
relation="task_site_contact_rel",
|
||||
column1="task_id",
|
||||
column2="res_partner_id",
|
||||
)
|
||||
|
||||
# Override related field to make it return false if this is an FSM subtask
|
||||
allow_billable = fields.Boolean(
|
||||
string="Can be billed",
|
||||
related=False,
|
||||
compute="_compute_allow_billable",
|
||||
store=True,
|
||||
)
|
||||
|
||||
visit_id = fields.Many2one(
|
||||
comodel_name="bemade_fsm.visit",
|
||||
string="Visit",
|
||||
)
|
||||
|
||||
relevant_order_lines = fields.Many2many(
|
||||
comodel_name="sale.order.line",
|
||||
store=False,
|
||||
compute="_compute_relevant_order_lines",
|
||||
)
|
||||
|
||||
work_order_number = fields.Char(readonly=True)
|
||||
|
||||
propagate_assignment = fields.Boolean(
|
||||
help="Propagate assignment of this task to all subtasks.",
|
||||
default=False,
|
||||
)
|
||||
|
||||
is_closed = fields.Boolean(
|
||||
compute="_compute_is_closed",
|
||||
)
|
||||
|
||||
root_ancestor = fields.Many2one(
|
||||
comodel_name="project.task",
|
||||
compute="_compute_root_ancestor",
|
||||
recursive=True,
|
||||
)
|
||||
|
||||
def _compute_is_closed(self):
|
||||
for rec in self:
|
||||
rec.is_closed = rec.state in CLOSED_STATES
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals):
|
||||
res = super().create(vals)
|
||||
for rec in res:
|
||||
if rec.parent_id and rec.is_fsm:
|
||||
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
|
||||
if not rec.site_contacts and rec.parent_id:
|
||||
rec.site_contacts = rec.parent_id.site_contacts
|
||||
if rec.sale_order_id:
|
||||
seq = 1
|
||||
prev_seqs = (
|
||||
self.sale_order_id.tasks_ids
|
||||
and self.sale_order_id.tasks_ids.mapped("work_order_number")
|
||||
)
|
||||
if prev_seqs:
|
||||
pattern = re.compile(r"(\d+)$")
|
||||
matches = map(lambda n: pattern.search(n), 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}"
|
||||
)
|
||||
# If the task is linked to a sales order and has no parent, it should inherit SO work order contacts
|
||||
if (
|
||||
not rec.parent_id
|
||||
and not rec.work_order_contacts
|
||||
and rec.sale_order_id.work_order_contacts
|
||||
):
|
||||
rec.work_order_contacts = rec.sale_order_id.work_order_contacts
|
||||
if (
|
||||
not rec.parent_id
|
||||
and not rec.site_contacts
|
||||
and rec.sale_order_id.site_contacts
|
||||
):
|
||||
rec.site_contacts = rec.sale_order_id.site_contacts
|
||||
return res
|
||||
|
||||
def write(self, vals):
|
||||
res = super().write(vals)
|
||||
if not self: # End recursion on empty RecordSet
|
||||
return res
|
||||
if "propagate_assignment" in vals:
|
||||
# When a user sets propagate assignment, it should propagate that setting all the way down the chain
|
||||
self.child_ids.write({"propagate_assignment": vals["propagate_assignment"]})
|
||||
if "user_ids" in vals:
|
||||
to_propagate = self.filtered(lambda task: task.propagate_assignment)
|
||||
# Here we use child_ids instead of _get_all_subtasks() so as to allow for setting propagate_assignment
|
||||
# to false on a child task.
|
||||
to_propagate.child_ids.write({"user_ids": vals["user_ids"]})
|
||||
for rec in self:
|
||||
if rec.child_ids:
|
||||
child_vals = {}
|
||||
if "site_contacts" in vals:
|
||||
child_vals.update(
|
||||
site_contacts=[Command.set(rec.site_contacts.ids)]
|
||||
)
|
||||
if "work_order_contacts" in vals:
|
||||
child_vals.update(
|
||||
work_order_contacts=[Command.set(rec.work_order_contacts.ids)]
|
||||
)
|
||||
if "partner_id" in vals:
|
||||
child_vals.update(partner_id=vals["partner_id"])
|
||||
rec.child_ids.write(child_vals)
|
||||
return res
|
||||
|
||||
@api.depends("sale_order_id")
|
||||
def _compute_relevant_order_lines(self):
|
||||
for rec in self:
|
||||
rec.relevant_order_lines = (
|
||||
rec.sale_order_id
|
||||
and rec.sale_order_id.get_relevant_order_lines(rec)
|
||||
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:
|
||||
# If an FSM task has a parent that is linked to an SO line, then the parent is the billable one
|
||||
if (
|
||||
rec.parent_id
|
||||
and not rec.parent_id.visit_id
|
||||
and rec.project_id
|
||||
and rec.project_id.is_fsm
|
||||
):
|
||||
rec.allow_billable = False
|
||||
else:
|
||||
rec.allow_billable = rec.project_id.allow_billable
|
||||
|
||||
def _fsm_create_sale_order_line(self):
|
||||
# Override to not generate new lines for tasks that are just linked to a visit item
|
||||
self.ensure_one()
|
||||
if self.visit_id:
|
||||
return
|
||||
else:
|
||||
super()._fsm_create_sale_order_line()
|
||||
|
||||
def action_fsm_validate(self, stop_running_timers=False):
|
||||
# Override to close out subtasks as well
|
||||
all_tasks = self | self._get_all_subtasks()
|
||||
return super(Task, all_tasks).action_fsm_validate(stop_running_timers)
|
||||
|
||||
def _get_full_hierarchy(self):
|
||||
if self.child_ids:
|
||||
return self | self.child_ids._get_full_hierarchy()
|
||||
return self
|
||||
|
||||
def synchronize_name_fsm(self):
|
||||
"""Applies naming to the entire task tree for tasks that are part of this
|
||||
recordset. Root tasks are named:
|
||||
|
||||
Partner Shipping Name - Sale Line Name (Template Name)
|
||||
|
||||
Child tasks with sale_line_id are named by their template if set, sale line name
|
||||
if not.
|
||||
|
||||
Child tasks not linked to sale lines are left with their original names."""
|
||||
|
||||
all_tasks = self | self._get_all_subtasks()
|
||||
for rec in all_tasks:
|
||||
assert rec.is_fsm, "This method should only be called on FSM tasks."
|
||||
|
||||
template = rec.sale_line_id and rec.sale_line_id.product_id.task_template_id
|
||||
|
||||
if template:
|
||||
title = template.name
|
||||
elif rec.sale_line_id:
|
||||
name_parts = rec.sale_line_id.name.split("\n")
|
||||
title = name_parts and name_parts[0] or rec.sale_line_id.product_id.name
|
||||
elif rec.visit_id:
|
||||
title = rec.visit_id.label
|
||||
else:
|
||||
rec.name = rec.name
|
||||
return
|
||||
|
||||
client_name = rec.sale_order_id.partner_shipping_id.name
|
||||
|
||||
if not rec.parent_id:
|
||||
rec.name = f"{rec.work_order_number} - {client_name} - {title}"
|
||||
else:
|
||||
rec.name = title
|
||||
|
||||
@api.depends("parent_id", "parent_id.root_ancestor")
|
||||
def _compute_root_ancestor(self):
|
||||
for rec in self:
|
||||
rec.root_ancestor = rec.parent_id and rec.parent_id.root_ancestor or self
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
from odoo import models, fields, api, Command
|
||||
|
||||
|
||||
class TaskTemplate(models.Model):
|
||||
_name = "project.task.template"
|
||||
_description = "Template for new project tasks"
|
||||
|
||||
@api.model
|
||||
def _current_company(self):
|
||||
return self.env.company
|
||||
|
||||
name = fields.Char(string="Task Title", required=True)
|
||||
|
||||
description = fields.Html()
|
||||
|
||||
assignees = fields.Many2many(
|
||||
comodel_name="res.users",
|
||||
string="Default Assignees",
|
||||
help="Employees assigned to tasks created from this template.",
|
||||
)
|
||||
|
||||
customer = fields.Many2one(
|
||||
comodel_name="res.partner",
|
||||
string="Default Customer",
|
||||
help="Default customer for tasks created from this template.",
|
||||
default=lambda self: self.parent.customer if self.parent else False,
|
||||
)
|
||||
|
||||
project = fields.Many2one(
|
||||
comodel_name="project.project",
|
||||
string="Default Project",
|
||||
help="Default project for tasks created from this template.",
|
||||
)
|
||||
|
||||
tags = fields.Many2many(
|
||||
comodel_name="project.tags",
|
||||
string="Default Tags",
|
||||
help="Default tags for tasks created from this template.",
|
||||
)
|
||||
|
||||
parent = fields.Many2one(
|
||||
comodel_name="project.task.template",
|
||||
string="Parent Task Template",
|
||||
ondelete="cascade",
|
||||
)
|
||||
|
||||
subtasks = fields.One2many(
|
||||
comodel_name="project.task.template",
|
||||
inverse_name="parent",
|
||||
string="Subtask Templates",
|
||||
)
|
||||
sequence = fields.Integer()
|
||||
|
||||
company_id = fields.Many2one(
|
||||
comodel_name="res.company", string="Company", index=1, default=_current_company
|
||||
)
|
||||
|
||||
planned_hours = fields.Float("Initially Planned Hours")
|
||||
|
||||
equipment_ids = fields.Many2many(
|
||||
comodel_name="fsm.equipment",
|
||||
relation="bemade_fsm_task_template_equipment_rel",
|
||||
column1="task_template_id",
|
||||
column2="equipment_id",
|
||||
string="Equipment to Service",
|
||||
)
|
||||
|
||||
def action_open_task(self):
|
||||
return {
|
||||
"view_mode": "form",
|
||||
"res_model": "project.task.template",
|
||||
"res_id": self.id,
|
||||
"type": "ir.actions.act_window",
|
||||
"context": self._context,
|
||||
}
|
||||
|
||||
@api.onchange("customer")
|
||||
def _onchange_customer(self):
|
||||
for rec in self:
|
||||
new_equipment_ids = [
|
||||
eq.id for eq in rec.equipment_ids if eq.partner_id == rec.customer
|
||||
]
|
||||
rec.write({"equipment_ids": [Command.set(new_equipment_ids)]})
|
||||
|
||||
def _prepare_new_task_values_from_self(self, project, name=False, parent_id=False):
|
||||
partner_id = self.customer or self.parent.customer or False
|
||||
if not partner_id and parent_id:
|
||||
parent = self.env["project.task"].browse(parent_id)
|
||||
partner_id = parent.partner_id or parent.sale_order_id.partner_shipping_id
|
||||
vals = {
|
||||
"project_id": project.id,
|
||||
"name": name or self.name,
|
||||
"description": self.description,
|
||||
"parent_id": parent_id,
|
||||
"user_ids": self.assignees.ids,
|
||||
"tag_ids": self.tags.ids,
|
||||
"allocated_hours": self.planned_hours,
|
||||
"sequence": self.sequence,
|
||||
"equipment_ids": (
|
||||
[Command.set(self.equipment_ids.ids)] if self.equipment_ids else False
|
||||
),
|
||||
"partner_id": partner_id and partner_id.id,
|
||||
"company_id": self.company_id.id,
|
||||
}
|
||||
return vals
|
||||
|
||||
def create_task_from_self(self, project, name=False, parent_id=False):
|
||||
"""Create a project.task from this template and return it.
|
||||
Can be called on a RecordSet of multiple templates.
|
||||
|
||||
:param project: project.project record the task should be added to
|
||||
:param name: name for the new task (defaults to template name)
|
||||
:param parent_id: parent task for the new task (none by default)
|
||||
:return: project.task record created from this template
|
||||
"""
|
||||
tasks = self.env["project.task"]
|
||||
for rec in self:
|
||||
vals = rec._prepare_new_task_values_from_self(project, name, parent_id)
|
||||
task = rec.env["project.task"].create(vals)
|
||||
rec.subtasks.create_task_from_self(project, name=False, parent_id=task.id)
|
||||
tasks |= task
|
||||
return tasks
|
||||
|
|
@ -1 +0,0 @@
|
|||
from . import worksheet_custom_reports
|
||||
|
|
@ -1,588 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<template id="workorder_page_materials_table">
|
||||
<t
|
||||
t-set="order_lines"
|
||||
t-value="doc.relevant_order_lines.filtered(lambda l:
|
||||
l.product_id.type != 'service' and not l.is_downpayment and not l.visit_id)"
|
||||
/>
|
||||
<t t-if="order_lines">
|
||||
<t t-set="visit_lines" t-value="order_lines.mapped('visit_id')" />
|
||||
<t
|
||||
t-set="section_lines"
|
||||
t-value="order_lines.filtered(lambda l: l.display_type == 'line_section')"
|
||||
/>
|
||||
</t>
|
||||
<div
|
||||
t-if="order_lines.filtered(lambda l: not l.display_type)"
|
||||
style="page-break-inside: avoid;"
|
||||
>
|
||||
<h2 t-if="order_lines">Materials</h2>
|
||||
<div t-if="order_lines" class="table-responsive-sm">
|
||||
<table class="table table-sm o_main_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left">Description</th>
|
||||
<th class="text-right">Ordered</th>
|
||||
<th class="text-right">Delivered</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="sale_tbody">
|
||||
<t t-foreach="order_lines" t-as="line">
|
||||
<t t-set="is_task" t-value="line == doc.sale_line_id" />
|
||||
<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 ''"
|
||||
>
|
||||
<t
|
||||
t-if="not line.display_type and not line.is_downpayment"
|
||||
>
|
||||
<td><span t-field="line.name" /></td>
|
||||
<td class="text-right">
|
||||
<span t-field="line.product_uom_qty" />
|
||||
<span
|
||||
t-field="line.product_uom"
|
||||
groups="uom.group_uom"
|
||||
/>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span t-field="line.qty_delivered" />
|
||||
<span
|
||||
t-field="line.product_uom"
|
||||
groups="uom.group_uom"
|
||||
/>
|
||||
</td>
|
||||
</t>
|
||||
<t t-if="line.display_type == 'line_section'">
|
||||
<td colspan="99">
|
||||
<span t-field="line.name" />
|
||||
</td>
|
||||
</t>
|
||||
<t t-if="line.display_type == 'line_note'">
|
||||
<td colspan="99">
|
||||
<span t-field="line.name" />
|
||||
</td>
|
||||
</t>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template id="workorder_page_sale_order_table_with_pricing_and_labour">
|
||||
<t t-set="order_lines" t-value="doc.relevant_order_lines" />
|
||||
<t t-if="order_lines">
|
||||
<t t-set="visit_lines" t-value="order_lines.mapped('visit_id')" />
|
||||
<t
|
||||
t-if="visit_lines"
|
||||
t-set="root_tasks"
|
||||
t-value="visit_lines.mapped('task_id')"
|
||||
/>
|
||||
<t
|
||||
t-else=""
|
||||
t-set="root_tasks"
|
||||
t-value="order_lines.mapped('task_id').filtered(lambda l: not l.parent_id)"
|
||||
/>
|
||||
<t t-set="final_subtotal" t-value="0" />
|
||||
<t t-set="final_tax" t-value="0" />
|
||||
<t t-set="final_total" t-value="0" />
|
||||
<t t-set="is_any_total_discount_line" t-value="False" />
|
||||
<t
|
||||
t-set="section_lines"
|
||||
t-value="order_lines.filtered(lambda l: l.display_type == 'line_section')"
|
||||
/>
|
||||
<t t-foreach="order_lines" t-as="line">
|
||||
<t
|
||||
t-set="final_subtotal"
|
||||
t-value="final_subtotal + line.delivered_price_subtotal"
|
||||
/>
|
||||
<t
|
||||
t-set="final_total"
|
||||
t-value="final_total + line.delivered_price_total"
|
||||
/>
|
||||
<t t-set="final_tax" t-value="final_tax + line.delivered_price_tax" />
|
||||
<t
|
||||
t-set="is_any_total_discount_line"
|
||||
t-value="is_any_total_discount_line or (line.discount and line.price_unit != 0 and line.delivered_price_total == 0)"
|
||||
/>
|
||||
</t>
|
||||
<t
|
||||
t-set="display_discount"
|
||||
t-value="any(line.discount for line in order_lines)"
|
||||
/>
|
||||
</t>
|
||||
<t t-else="" t-set="root_tasks" t-value="doc.root_ancestor" />
|
||||
<div t-if="order_lines.filtered(lambda l: not l.display_type)">
|
||||
<h2 t-if="order_lines">Time & Material</h2>
|
||||
<div t-if="order_lines" class="table-responsive-sm">
|
||||
<table class="table table-sm o_main_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left">Description</th>
|
||||
<th class="text-right">Ordered</th>
|
||||
<th class="text-right">Delivered</th>
|
||||
<th class="text-right">Unit Price</th>
|
||||
<th
|
||||
t-if="display_discount"
|
||||
class="text-right"
|
||||
groups="product.group_discount_per_so_line"
|
||||
>
|
||||
<span>Disc.%</span>
|
||||
</th>
|
||||
<th class="text-right">
|
||||
<span
|
||||
groups="account.group_show_line_subtotals_tax_excluded"
|
||||
>
|
||||
Amount</span>
|
||||
<span
|
||||
groups="account.group_show_line_subtotals_tax_included"
|
||||
>
|
||||
Total Price</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="sale_tbody">
|
||||
<t t-set="current_subtotal" t-value="0" />
|
||||
<t t-foreach="order_lines" t-as="line">
|
||||
<t t-set="is_task" t-value="line == doc.sale_line_id" />
|
||||
<t
|
||||
t-set="is_total_discount"
|
||||
t-value="line.discount and line.price_unit != 0 and line.delivered_price_total == 0"
|
||||
/>
|
||||
<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 ''"
|
||||
>
|
||||
<t
|
||||
t-if="not line.display_type and not line.is_downpayment"
|
||||
>
|
||||
<td><span t-field="line.name" /></td>
|
||||
<td class="text-right">
|
||||
<span t-field="line.product_uom_qty" />
|
||||
<span
|
||||
t-field="line.product_uom"
|
||||
groups="uom.group_uom"
|
||||
/>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span t-field="line.qty_delivered" />
|
||||
<span
|
||||
t-field="line.product_uom"
|
||||
groups="uom.group_uom"
|
||||
/>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<span t-field="line.price_unit" />
|
||||
</td>
|
||||
<td
|
||||
t-if="display_discount"
|
||||
class="text-right"
|
||||
groups="product.group_discount_per_so_line"
|
||||
>
|
||||
<span t-field="line.discount" />
|
||||
</td>
|
||||
<td class="text-right o_price_total">
|
||||
<span t-field="line.delivered_price_subtotal" />
|
||||
</td>
|
||||
</t>
|
||||
<t t-if="line.display_type == 'line_section'">
|
||||
<td colspan="99">
|
||||
<span t-field="line.name" />
|
||||
</td>
|
||||
<t t-set="current_section" t-value="line" />
|
||||
<t t-set="current_subtotal" t-value="0" />
|
||||
</t>
|
||||
<t t-if="line.display_type == 'line_note'">
|
||||
<td colspan="99">
|
||||
<span t-field="line.name" />
|
||||
</td>
|
||||
</t>
|
||||
</tr>
|
||||
<t
|
||||
t-if="current_section and len(section_lines) > 1 and (line_last or order_lines[line_index+1].display_type == 'line_section')"
|
||||
>
|
||||
<tr class="is-subtotal text-right">
|
||||
<td colspan="99">
|
||||
<strong class="mr16">Section
|
||||
Subtotal</strong>
|
||||
<span
|
||||
t-esc="current_subtotal"
|
||||
t-options='{"widget": "monetary", "display_currency": doc.sale_order_id.pricelist_id.currency_id}'
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="container_subtotal" name="so_total_summary">
|
||||
<div id="total" class="row justify-content-end" name="total">
|
||||
<div
|
||||
t-attf-class="#{'col-auto' if report_type != 'html' else 'col-sm-2'}"
|
||||
>
|
||||
<table class="table table-sm">
|
||||
<tr
|
||||
t-if="final_tax"
|
||||
class="border-black o_subtotal"
|
||||
style=""
|
||||
>
|
||||
<td><strong>Untaxed amount</strong></td>
|
||||
<td class="text-right">
|
||||
<span
|
||||
t-esc="final_subtotal"
|
||||
t-options='{"widget": "monetary", "display_currency": doc.sale_order_id.pricelist_id.currency_id}'
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr t-if="final_tax" class="border-black o_subtotal">
|
||||
<td><strong>Taxes</strong></td>
|
||||
<td class="text-right">
|
||||
<span
|
||||
t-esc="final_tax"
|
||||
t-options='{"widget": "monetary", "display_currency": doc.sale_order_id.pricelist_id.currency_id}'
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr t-if="final_total" class="border-black o_total">
|
||||
<td><strong>Total</strong></td>
|
||||
<td class="text-right">
|
||||
<span
|
||||
t-esc="final_total"
|
||||
t-options='{"widget": "monetary", "display_currency": doc.sale_order_id.pricelist_id.currency_id}'
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template id="subtask_list">
|
||||
<ul t-if="task.child_ids" class="ml-1 pl-1" style="list-style-type: none;">
|
||||
<t t-foreach="task.child_ids" t-as="subtask">
|
||||
<li class="ml-1">
|
||||
<span t-out="subtask.name" />
|
||||
<t t-call="bemade_fsm.subtask_list">
|
||||
<t t-set="task" t-value="subtask" />
|
||||
</t>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
</template>
|
||||
<template id="workorder_page_tasks_table">
|
||||
<t t-set="interventions" t-value="doc.child_ids" />
|
||||
<t t-foreach="interventions" t-as="intervention">
|
||||
<div style="page-break-inside: avoid;">
|
||||
<h2 t-out="str(intervention_index + 1) + '. ' + intervention.name" />
|
||||
<h3 t-if="intervention.equipment_ids">Equipment:
|
||||
<t t-foreach="intervention.equipment_ids" t-as="equipment_id">
|
||||
<span
|
||||
t-out="equipment_id.display_name + (', ' if not equipment_id_last else '')"
|
||||
/>
|
||||
</t>
|
||||
</h3>
|
||||
<div>
|
||||
<span t-esc="intervention.description" />
|
||||
</div>
|
||||
<t t-set="tasks" t-value="intervention.child_ids" />
|
||||
<div t-if="tasks" class="table-responsive-sm">
|
||||
<table class="table table-sm o_main_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="7%">Status</th>
|
||||
<th width="39%">Task</th>
|
||||
<th width="54%">Comments</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="tasks" t-as="task">
|
||||
<tr class="p-1 m-1">
|
||||
<td t-out="task.stage_id.name" />
|
||||
<td>
|
||||
<span t-out="task.name" />
|
||||
</td>
|
||||
<td t-out="task.description or ''" />
|
||||
</tr>
|
||||
<tr t-if="task.child_ids" class="mt-0 pt-0">
|
||||
<td />
|
||||
<td>
|
||||
<t t-call="bemade_fsm.subtask_list" />
|
||||
</td>
|
||||
<td />
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</template>
|
||||
<template id="workorder_page_info_block">
|
||||
<h1>
|
||||
<span name="work_order_number" t-out="doc.name" />
|
||||
</h1>
|
||||
<h4 t-if="doc.sale_order_id and doc.sale_order_id.client_order_ref">
|
||||
Purchase Order: <span
|
||||
name="po_number"
|
||||
t-out="doc.sale_order_id.client_order_ref"
|
||||
/>
|
||||
</h4>
|
||||
<hr />
|
||||
<div class="row" name="address_and_time">
|
||||
<div
|
||||
t-attf-class="{{('col-6' if report_type == 'pdf' else 'col-md-6 col-12') + ' mb-3'}}"
|
||||
>
|
||||
<t t-if="doc.partner_id">
|
||||
<div><h6>Customer: </h6></div>
|
||||
<div
|
||||
t-esc="doc.partner_id"
|
||||
t-options='{
|
||||
"widget": "contact",
|
||||
"fields": ["name", "address",]
|
||||
}'
|
||||
/>
|
||||
</t>
|
||||
</div>
|
||||
<div
|
||||
t-attf-class="{{('col-6' if report_type == 'pdf' else 'col-md-6 col-12') + ' mb-3'}}"
|
||||
t-if="doc.planned_date_begin or doc.date_deadline"
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" name="site_and_work_order_contacts">
|
||||
<div
|
||||
t-attf-class="{{'col-6' if report_type == 'pdf' else 'col-md-6 col-12'}}"
|
||||
>
|
||||
<div t-if="doc.site_contacts"><h6>Site Contacts: </h6></div>
|
||||
<t t-foreach="doc.site_contacts" t-as="contact">
|
||||
<div class="mb-3">
|
||||
<div
|
||||
t-esc="contact"
|
||||
t-options='{
|
||||
"widget": "contact",
|
||||
"fields": ["name", "phone", "email"]
|
||||
}'
|
||||
/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<div
|
||||
t-attf-class="{{'col-6' if report_type == 'pdf' else 'col-md-6 col-12'}}"
|
||||
>
|
||||
<div t-if="doc.work_order_contacts"><h6>Work Order
|
||||
Contacts: </h6></div>
|
||||
<t t-foreach="doc.work_order_contacts" t-as="contact">
|
||||
<div class="mb-3">
|
||||
<div
|
||||
t-esc="contact"
|
||||
t-options='{
|
||||
"widget": "contact",
|
||||
"fields": ["name", "phone", "email"]
|
||||
}'
|
||||
/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div
|
||||
t-if="not is_html_empty(doc.description)"
|
||||
class="row"
|
||||
name="visit_description"
|
||||
>
|
||||
<div class="col-12">
|
||||
<span t-out="doc.description" />
|
||||
</div>
|
||||
</div>
|
||||
<hr t-if="not is_html_empty(doc.description)" />
|
||||
</template>
|
||||
<template id="workorder_equipment_summary">
|
||||
<div
|
||||
t-if="doc.equipment_ids and len(doc.child_ids) > 1"
|
||||
style="page-break-inside: avoid;"
|
||||
>
|
||||
<h3>Equipment Serviced</h3>
|
||||
<div class="table-responsive-sm">
|
||||
<table class="table table-sm o_main_table overflow-hidden">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tag</th>
|
||||
<th>Name</th>
|
||||
<th>Application</th>
|
||||
<th>Location</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="doc.equipment_ids" t-as="equipment">
|
||||
<tr>
|
||||
<td><span t-esc="equipment.code" /></td>
|
||||
<td><span t-esc="equipment.name" /></td>
|
||||
<td>
|
||||
<t t-foreach="equipment.tag_ids" t-as="application">
|
||||
<span
|
||||
t-esc="application.name + (', ' if not application_last else '')"
|
||||
/>
|
||||
</t>
|
||||
</td>
|
||||
<td
|
||||
t-esc="equipment.partner_id"
|
||||
t-options='{
|
||||
"widget": "contact",
|
||||
"fields": ["name", "address"]}'
|
||||
/>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template id="workorder_page_timesheet_entries">
|
||||
<t t-set="timesheets" t-value="doc.timesheet_ids" />
|
||||
<div t-if="timesheets" style="page-break-inside: avoid;">
|
||||
<h1>Time</h1>
|
||||
<div class="table-responsive-sm">
|
||||
<table class="table table-sm o_main_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="14%">Date</th>
|
||||
<th width="30%">Technician</th>
|
||||
<th width="49%">Work Completed</th>
|
||||
<th width="7%" class="text-right">Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="timesheets" t-as="timesheet" class="p-1 m-1">
|
||||
<td>
|
||||
<span t-out="timesheet.date.strftime('%Y-%m-%d')" />
|
||||
</td>
|
||||
<td>
|
||||
<span t-field="timesheet.employee_id.name" />
|
||||
</td>
|
||||
<td>
|
||||
<span t-field="timesheet.name" />
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
t-esc="timesheet.unit_amount"
|
||||
t-options="{
|
||||
'widget': 'duration',
|
||||
'digital': True,
|
||||
'unit': 'hour',
|
||||
'round': 'minute'
|
||||
}"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template id="workorder_page_signature_block">
|
||||
<div t-if="doc.worksheet_signature">
|
||||
<div
|
||||
t-if="report_type == html"
|
||||
class="ribbon"
|
||||
style="
|
||||
position: absolute;
|
||||
right: 0px; top: 0px;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
width: 75px; height: 75px;
|
||||
text-align: right;"
|
||||
>
|
||||
<span
|
||||
style="
|
||||
font-size: 10px;
|
||||
color: #fff;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
font-weight: bold; line-height: 20px;
|
||||
transform: rotate(45deg);
|
||||
width: 100px; height: auto; display: block;
|
||||
background: green;
|
||||
position: absolute;
|
||||
top: 19px; right: -21px; left: auto;
|
||||
padding: 0;"
|
||||
>
|
||||
Signed
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
t-attf-class="#{' col-12 col-lg-3' if report_type != 'html' else '
|
||||
col-sm-7 col-md-4'} ml-auto text-right"
|
||||
style="page-break-inside: avoid"
|
||||
>
|
||||
<h5>Signature</h5>
|
||||
<img
|
||||
t-att-src="image_data_uri(doc.worksheet_signature)"
|
||||
style="max-height: 6rem; max-width: 100%; color:black;"
|
||||
/><br />
|
||||
<span t-field="doc.worksheet_signed_by" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template id="work_order_page">
|
||||
<div class="page">
|
||||
<t t-call="bemade_fsm.workorder_page_info_block" />
|
||||
<t t-call="bemade_fsm.workorder_page_timesheet_entries" />
|
||||
<t t-if="split_time_materials">
|
||||
<t t-call="bemade_fsm.workorder_page_materials_table" />
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t
|
||||
t-call="bemade_fsm.workorder_page_sale_order_table_with_pricing_and_labour"
|
||||
/>
|
||||
</t>
|
||||
<t t-call="bemade_fsm.workorder_equipment_summary" />
|
||||
<t t-call="bemade_fsm.workorder_page_signature_block" />
|
||||
<t t-call="bemade_fsm.workorder_page_tasks_table" />
|
||||
</div>
|
||||
</template>
|
||||
<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-call="web.external_layout">
|
||||
<t
|
||||
t-call="bemade_fsm.work_order_page"
|
||||
t-lang="doc.partner_id.lang"
|
||||
/>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
<template
|
||||
id="worksheet_custom"
|
||||
inherit_id="industry_fsm_report.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>
|
||||
</template>
|
||||
</odoo>
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
from odoo import models
|
||||
|
||||
|
||||
class TaskCustomReport(models.AbstractModel):
|
||||
_inherit = "report.industry_fsm_report.worksheet_custom"
|
||||
|
||||
def _get_report_values(self, docids, data=None):
|
||||
vals = super()._get_report_values(docids, data)
|
||||
split_time_materials = (
|
||||
self.env.company.split_time_from_materials_on_service_work_orders
|
||||
)
|
||||
vals.update({"split_time_materials": split_time_materials})
|
||||
return vals
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="industry_fsm_report.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'),
|
||||
object.name
|
||||
)
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_bemade_fsm_task_template_manager,bemade_fsm_task_template,model_project_task_template,project.group_project_manager,1,1,1,1
|
||||
access_bemade_fsm_task_template_user,bemade_fsm_task_template,model_project_task_template,project.group_project_user,1,1,1,1
|
||||
access_bemade_fsm_visit,access_bemade_fsm_visit,model_bemade_fsm_visit,base.group_user,1,1,1,1
|
||||
access_bemade_fsm_task_wizard,access_bemade_fsm_task_wizard,model_project_task_from_template_wizard,project.group_project_user,1,1,1,1
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import KanbanController from 'web.KanbanController';
|
||||
import KanbanView from 'web.KanbanView';
|
||||
const viewRegistry = require('web.view_registry')
|
||||
|
||||
const ProjectKanbanController = KanbanController.extend({
|
||||
buttons_template: 'project.KanbanView.buttons',
|
||||
custom_events: _.extend({}, KanbanController.prototype.custom_events, {
|
||||
create_from_template: '_onCreateFromTemplate',
|
||||
}),
|
||||
_onCreateFromTemplate(ev) {
|
||||
ev.stopPropagation();
|
||||
const record = this.model.get(this.handle, {raw: true});
|
||||
let context = record.context;
|
||||
context.active_id = record.project_id;
|
||||
this.do_action({
|
||||
type: 'ir.actions.act_window',
|
||||
res_model: 'project.task.from.template.wizard',
|
||||
views: [[false, 'form']],
|
||||
target: 'new',
|
||||
context: {...record.context, res_model: 'project.task'},
|
||||
});
|
||||
},
|
||||
renderButtons ($node) {
|
||||
this._super.apply(this, arguments);
|
||||
this.$buttons.on('click', 'button.o-kanban-button-new-from-template', this._onCreateFromTemplate.bind(this))
|
||||
},
|
||||
})
|
||||
|
||||
const ProjectKanbanView = KanbanView.extend({
|
||||
config: _.extend({}, KanbanView.prototype.config, {
|
||||
Controller: ProjectKanbanController,
|
||||
})
|
||||
});
|
||||
|
||||
viewRegistry.add('project_kanban', ProjectKanbanView)
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import ListController from 'web.ListController';
|
||||
import ListView from 'web.ListView';
|
||||
const viewRegistry = require('web.view_registry')
|
||||
|
||||
const ProjectListController = ListController.extend({
|
||||
buttons_template: 'project.ListView.buttons',
|
||||
custom_events: _.extend({}, ListController.prototype.custom_events, {
|
||||
create_from_template: '_onCreateFromTemplate',
|
||||
}),
|
||||
_onCreateFromTemplate(ev) {
|
||||
ev.stopPropagation();
|
||||
const record = this.model.get(this.handle, {raw: true});
|
||||
this.do_action({
|
||||
type: 'ir.actions.act_window',
|
||||
res_model: 'project.task.from.template.wizard',
|
||||
views: [[false, 'form']],
|
||||
target: 'new',
|
||||
context: {...record.context, res_model: 'project.task'},
|
||||
});
|
||||
},
|
||||
renderButtons ($node) {
|
||||
self = this;
|
||||
this._super.apply(this, arguments);
|
||||
this.$buttons.on('click', 'button.o_list_button_add_from_template', this._onCreateFromTemplate.bind(this))
|
||||
},
|
||||
})
|
||||
|
||||
const ProjectListView = ListView.extend({
|
||||
config: _.extend({}, ListView.prototype.config, {
|
||||
Controller: ProjectListController,
|
||||
})
|
||||
});
|
||||
|
||||
viewRegistry.add('project_list', ProjectListView)
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
/* This prevents repeating the header, which overlaps with the body due to a bug in
|
||||
wkhtmltopdf rendering.
|
||||
*/
|
||||
thead {
|
||||
display: table-row-group;
|
||||
}
|
||||
|
||||
tfoot {
|
||||
display: table-row-group;
|
||||
}
|
||||
|
||||
tr {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
<?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">
|
||||
<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">
|
||||
<t t-esc="create_text && (create_text + ' from Template') || _t('Create from Template')"/>
|
||||
</button>
|
||||
</xpath>
|
||||
</t>
|
||||
<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"
|
||||
title="Create Task from Template">
|
||||
Create from Template
|
||||
</button>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
from . import test_bemade_fsm_common
|
||||
from . import test_task_template
|
||||
from . import test_sale_order
|
||||
from . import test_fsm_contact_setting
|
||||
from . import test_fsm_visit
|
||||
from . import test_task
|
||||
from . import test_task_report
|
||||
from . import test_settings
|
||||
from . import test_equipment
|
||||
|
|
@ -1,284 +0,0 @@
|
|||
from odoo.tests.common import TransactionCase, tagged
|
||||
from odoo import Command
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class BemadeFSMBaseTest(TransactionCase):
|
||||
@classmethod
|
||||
def _generate_project_manager_user(cls, name, login):
|
||||
group_ids = cls.__get_user_groups()
|
||||
user_group_project_manager = cls.env.ref("project.group_project_manager")
|
||||
user_group_fsm_manager = cls.env.ref("industry_fsm.group_fsm_manager")
|
||||
group_ids.append(user_group_fsm_manager.id)
|
||||
group_ids.append(user_group_project_manager.id)
|
||||
|
||||
return cls.__generate_user(name, login, group_ids)
|
||||
|
||||
@classmethod
|
||||
def _generate_project_user(cls, name, login):
|
||||
group_ids = cls.__get_user_groups()
|
||||
return cls.__generate_user(name, login, group_ids)
|
||||
|
||||
@classmethod
|
||||
def __generate_user(cls, name, login, group_ids):
|
||||
return (
|
||||
cls.env["res.users"]
|
||||
.with_context(no_reset_password=True)
|
||||
.create(
|
||||
{
|
||||
"name": name,
|
||||
"login": login,
|
||||
"password": login,
|
||||
"email": f"{login}@test.co",
|
||||
"groups_id": [Command.set(group_ids)],
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def __get_user_groups(cls):
|
||||
user_group_employee = cls.env.ref("base.group_user")
|
||||
user_group_project_user = cls.env.ref("project.group_project_user")
|
||||
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_product_customer = cls.env.ref(
|
||||
"customer_product_code.group_product_customer_code_user",
|
||||
raise_if_not_found=False,
|
||||
)
|
||||
|
||||
group_ids = [
|
||||
user_group_employee.id,
|
||||
user_group_project_user.id,
|
||||
user_group_fsm_user.id,
|
||||
user_group_sales_manager.id,
|
||||
user_group_sales_user.id,
|
||||
]
|
||||
if user_product_customer:
|
||||
group_ids.append(user_product_customer.id)
|
||||
return group_ids
|
||||
|
||||
@classmethod
|
||||
def _generate_partner(
|
||||
cls,
|
||||
name: str = "Test Company",
|
||||
company_type: str = "company",
|
||||
parent=None,
|
||||
location_type="other",
|
||||
):
|
||||
"""Generates a partner with basic address filled in.
|
||||
|
||||
:param name: The partner's name.
|
||||
:param company_type: The type of partner, either 'company' or 'person' are
|
||||
accepted.
|
||||
"""
|
||||
return cls.env["res.partner"].create(
|
||||
{
|
||||
"name": name,
|
||||
"company_type": company_type,
|
||||
"street": "123 Street St.",
|
||||
"city": "Montreal",
|
||||
"state_id": cls.env.ref("base.state_ca_qc").id,
|
||||
"country_id": cls.env.ref("base.ca").id,
|
||||
"parent_id": parent and parent.id or False,
|
||||
"type": location_type,
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _generate_sale_order(
|
||||
cls,
|
||||
partner=None,
|
||||
client_order_ref="Test Order",
|
||||
equipment=None,
|
||||
shipping_location=None,
|
||||
):
|
||||
partner = partner or cls._generate_partner()
|
||||
vals = {"partner_id": partner.id, "client_order_ref": client_order_ref}
|
||||
if equipment:
|
||||
vals.update({"default_equipment_ids": [Command.set([equipment.id])]})
|
||||
if shipping_location:
|
||||
vals.update({"partner_shipping_id": shipping_location.id})
|
||||
return cls.env["sale.order"].create(vals)
|
||||
|
||||
@classmethod
|
||||
def _generate_sale_order_line(
|
||||
cls, sale_order, product=None, qty=1.0, uom=None, price=100.0, tax_id=False
|
||||
):
|
||||
if not product:
|
||||
product = cls._generate_product()
|
||||
return cls.env["sale.order.line"].create(
|
||||
{
|
||||
"order_id": sale_order.id,
|
||||
"product_id": product.id,
|
||||
"product_uom_qty": qty,
|
||||
"product_uom": uom and uom.id or cls.env.ref("uom.product_uom_hour").id,
|
||||
"price_unit": price,
|
||||
"tax_id": tax_id,
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _generate_equipment(cls, name="test equipment", partner=None):
|
||||
return cls.env["fsm.equipment"].create(
|
||||
{
|
||||
"name": name,
|
||||
"partner_id": partner and partner.id or False,
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _generate_product(
|
||||
cls,
|
||||
name="Test Product",
|
||||
product_type="service",
|
||||
service_tracking="task_global_project",
|
||||
project=None,
|
||||
task_template=None,
|
||||
service_policy="delivered_manual",
|
||||
uom=None,
|
||||
):
|
||||
if "project" in service_tracking and not project:
|
||||
project = cls.env.ref("industry_fsm.fsm_project")
|
||||
uom_id = uom and uom.id or cls.env.ref("uom.product_uom_hour").id or False
|
||||
return cls.env["product.product"].create(
|
||||
{
|
||||
"name": name,
|
||||
"type": product_type,
|
||||
"service_tracking": service_tracking,
|
||||
"service_type": "timesheet",
|
||||
"project_id": (
|
||||
service_tracking in ("task_global_project", "project_only")
|
||||
and project.id
|
||||
or False
|
||||
),
|
||||
"project_template_id": (
|
||||
service_tracking == "task_in_project" and project.id or False
|
||||
),
|
||||
"task_template_id": task_template and task_template.id or False,
|
||||
"service_policy": service_policy,
|
||||
"uom_id": uom_id,
|
||||
"uom_po_id": uom_id,
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _generate_fsm_project(cls, name="Test Project"):
|
||||
return cls.env["project.project"].create(
|
||||
{
|
||||
"name": name,
|
||||
"allow_material": True,
|
||||
"allow_timesheets": True,
|
||||
"allow_quotations": True,
|
||||
"allow_worksheets": True,
|
||||
"is_fsm": True,
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _generate_task_template(
|
||||
cls,
|
||||
parent=None,
|
||||
structure=None,
|
||||
names=None,
|
||||
planned_hours=1,
|
||||
equipment=None,
|
||||
customer=None,
|
||||
):
|
||||
"""Generates a task template with the specified structure and naming.
|
||||
|
||||
:param parent: The parent task template for the top-level task template being
|
||||
generated
|
||||
:param structure: A list of integers describing the number of tasks for each
|
||||
level of descendants. An empty list represents only one
|
||||
top-level task template. If no structure is given, an empty
|
||||
list will be used in its place.
|
||||
:param names: The name prefixes to be given to the task templates at each level.
|
||||
Each prefix will be followed by a sequential integer for its
|
||||
level. Child 1, Child 2, Grandchild 1, etc. If no names argument
|
||||
is passed, a default ['Task Template'] argument will be used.
|
||||
:param planned_hours: The number of planned hours for the top-level task
|
||||
template being generated.
|
||||
:param equipment: The equipment to add as linked equipment to the task template.
|
||||
"""
|
||||
if not names:
|
||||
names = ["Task Template"]
|
||||
if not structure:
|
||||
structure = []
|
||||
if len(structure) != len(names) - 1:
|
||||
raise ValueError(
|
||||
"The length of the structure argument must contain exactly one element"
|
||||
" less than the names argument."
|
||||
)
|
||||
name = names.pop(0)
|
||||
template = cls.env["project.task.template"].create(
|
||||
{
|
||||
"name": name,
|
||||
"parent": parent and parent.id or False,
|
||||
"planned_hours": planned_hours,
|
||||
"equipment_ids": [Command.set(equipment and [equipment.id] or [])],
|
||||
"customer": customer and customer.id or False,
|
||||
}
|
||||
)
|
||||
parent = template
|
||||
while structure:
|
||||
subtasks = []
|
||||
for i in range(0, structure[0]):
|
||||
subtasks.append(
|
||||
cls.env["project.task.template"].create(
|
||||
{
|
||||
"parent": parent.id,
|
||||
"name": names[0] + f" {i}",
|
||||
}
|
||||
)
|
||||
)
|
||||
structure.pop(0)
|
||||
names.pop(0)
|
||||
parent = subtasks[0]
|
||||
return template
|
||||
|
||||
def _invoice_sale_order(self, so):
|
||||
wiz = (
|
||||
self.env["sale.advance.payment.inv"]
|
||||
.with_context(active_ids=[so.id])
|
||||
.create({})
|
||||
)
|
||||
wiz.create_invoices()
|
||||
inv = so.invoice_ids[-1]
|
||||
inv.action_post()
|
||||
return inv
|
||||
|
||||
def _generate_visit(self, sale_order, label="Test Label"):
|
||||
return self.env["bemade_fsm.visit"].create(
|
||||
[
|
||||
{
|
||||
"sale_order_id": sale_order.id,
|
||||
"label": label,
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
def _generate_so_with_one_visit_two_lines(self):
|
||||
so = self._generate_sale_order()
|
||||
visit = self._generate_visit(sale_order=so)
|
||||
sol1 = self._generate_sale_order_line(sale_order=so)
|
||||
sol2 = self._generate_sale_order_line(sale_order=so)
|
||||
visit.so_section_id.sequence = 1
|
||||
sol1.sequence = 2
|
||||
sol2.sequence = 3
|
||||
return so, visit, sol1, sol2
|
||||
|
||||
def _generate_so_with_one_visit_two_lines_and_descendants(self):
|
||||
so = self._generate_sale_order()
|
||||
visit = self._generate_visit(sale_order=so)
|
||||
task_template = self._generate_task_template(
|
||||
structure=[2, 2, 2],
|
||||
names=["Parent", "Child", "Grandchild", "Great-grandchild"],
|
||||
)
|
||||
product = self._generate_product(task_template=task_template)
|
||||
sol1 = self._generate_sale_order_line(sale_order=so, product=product)
|
||||
sol2 = self._generate_sale_order_line(sale_order=so, product=product)
|
||||
visit.so_section_id.sequence = 1
|
||||
sol1.sequence = 2
|
||||
sol2.sequence = 3
|
||||
return so, visit, sol1, sol2
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
from odoo.addons.bemade_fsm.tests.test_bemade_fsm_common import BemadeFSMBaseTest
|
||||
from odoo.tests import tagged, Form
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class TestEquipment(BemadeFSMBaseTest):
|
||||
def test_equipment_search_domain_on_sale_order(self):
|
||||
"""Equipment from other clients was showing up in sale order line
|
||||
equipment choices. Make sure this doesn't happen."""
|
||||
partner = self._generate_partner()
|
||||
partner_2 = self._generate_partner()
|
||||
equipment_1 = self._generate_equipment(partner=partner)
|
||||
equipment_2 = self._generate_equipment(partner_2)
|
||||
sale_order = self._generate_sale_order(partner=partner)
|
||||
product = self._generate_product()
|
||||
self.assertEqual(sale_order.valid_equipment_ids, equipment_1)
|
||||
|
||||
name_search_results = self.env["fsm.equipment"].name_search(
|
||||
args=[
|
||||
"&",
|
||||
["id", "in", sale_order.valid_equipment_ids.ids],
|
||||
"!",
|
||||
["id", "in", []],
|
||||
],
|
||||
limit=8,
|
||||
name="test",
|
||||
operator="ilike",
|
||||
)
|
||||
self.assertNotIn(
|
||||
(equipment_2.id, equipment_2.display_name), name_search_results
|
||||
)
|
||||
self.assertIn((equipment_1.id, equipment_1.display_name), name_search_results)
|
||||
|
|
@ -1,129 +0,0 @@
|
|||
from odoo.tests import tagged, Form
|
||||
from odoo import Command
|
||||
from .test_bemade_fsm_common import BemadeFSMBaseTest
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class SaleOrderFSMContactsCase(BemadeFSMBaseTest):
|
||||
def test_site_contacts(self):
|
||||
parent_co = self._generate_partner("Parent Co")
|
||||
contact_1 = self._generate_partner("Contact 1", "person", parent_co)
|
||||
contact_2 = self._generate_partner("Contact 2", "person", parent_co)
|
||||
|
||||
# Make sure the SO pulls the defaults from the partner correctly
|
||||
parent_co.write({"site_contacts": [Command.set([contact_1.id, contact_2.id])]})
|
||||
so = self._generate_sale_order(parent_co)
|
||||
self.assertTrue(so.site_contacts == parent_co.site_contacts)
|
||||
|
||||
# Make sure updating the site contacts on the SO doesn't feed back to the
|
||||
# partner
|
||||
so.write({"site_contacts": [Command.set([contact_1.id])]})
|
||||
self.assertTrue(contact_1 in so.site_contacts)
|
||||
self.assertTrue(contact_2 not in so.site_contacts)
|
||||
self.assertTrue(
|
||||
so.site_contacts != so.partner_id.site_contacts
|
||||
and len(so.partner_id.site_contacts) == 2
|
||||
)
|
||||
|
||||
def test_default_workorder_contacts(self):
|
||||
parent_co = self._generate_partner("Parent Co")
|
||||
contact_1 = self._generate_partner("Contact 1", "person", parent_co)
|
||||
contact_2 = self._generate_partner("Contact 2", "person", parent_co)
|
||||
|
||||
# Make sure the SO pulls the defaults from the partner correctly
|
||||
parent_co.write(
|
||||
{"work_order_contacts": [Command.set([contact_1.id, contact_2.id])]}
|
||||
)
|
||||
so = self._generate_sale_order(parent_co)
|
||||
self.assertTrue(contact_1 in parent_co.work_order_contacts)
|
||||
self.assertTrue(contact_2 in parent_co.work_order_contacts)
|
||||
self.assertTrue(contact_1 in so.work_order_contacts)
|
||||
self.assertTrue(contact_2 in so.work_order_contacts)
|
||||
|
||||
# Make sure setting the work order contacts on the SO doesn't feed back to the
|
||||
# partner
|
||||
so.write({"work_order_contacts": [Command.set([contact_1.id])]})
|
||||
self.assertTrue(contact_1 in so.work_order_contacts)
|
||||
self.assertTrue(contact_2 not in so.work_order_contacts)
|
||||
self.assertTrue(
|
||||
so.work_order_contacts != so.partner_id.work_order_contacts
|
||||
and len(so.partner_id.work_order_contacts) == 2
|
||||
)
|
||||
|
||||
def test_multilayer_site_contacts(self):
|
||||
parent_co = self._generate_partner("Parent Co")
|
||||
shipping_location = self._generate_partner(
|
||||
"Shipping Location", "company", parent_co, "delivery"
|
||||
)
|
||||
wo_contact_1 = self._generate_partner(
|
||||
"WO Contact 1", "person", shipping_location
|
||||
)
|
||||
wo_contact_2 = self._generate_partner(
|
||||
"WO Contact 2", "person", shipping_location
|
||||
)
|
||||
site_contact_1 = self._generate_partner(
|
||||
"Site Contact 1", "person", shipping_location
|
||||
)
|
||||
site_contact_2 = self._generate_partner(
|
||||
"Site Contact 2", "person", shipping_location
|
||||
)
|
||||
shipping_location.write(
|
||||
{
|
||||
"work_order_contacts": [
|
||||
Command.set([wo_contact_1.id, wo_contact_2.id])
|
||||
],
|
||||
"site_contacts": [Command.set([site_contact_1.id, site_contact_2.id])],
|
||||
}
|
||||
)
|
||||
|
||||
so = self._generate_sale_order(parent_co)
|
||||
so.write({"partner_shipping_id": shipping_location.id})
|
||||
|
||||
self.assertEqual(so.site_contacts, shipping_location.site_contacts)
|
||||
self.assertEqual(so.work_order_contacts, shipping_location.work_order_contacts)
|
||||
|
||||
def test_onchange_shipping_address(self):
|
||||
self.env.user.groups_id += self.env.ref(
|
||||
"account.group_delivery_invoice_address"
|
||||
)
|
||||
parent_co = self._generate_partner("Parent Co")
|
||||
shipping_location = self._generate_partner(
|
||||
"Shipping Location", "company", parent_co, "delivery"
|
||||
)
|
||||
wo_contact_1 = self._generate_partner(
|
||||
"WO Contact 1", "person", shipping_location
|
||||
)
|
||||
wo_contact_2 = self._generate_partner(
|
||||
"WO Contact 2", "person", shipping_location
|
||||
)
|
||||
site_contact_1 = self._generate_partner(
|
||||
"Site Contact 1", "person", shipping_location
|
||||
)
|
||||
site_contact_2 = self._generate_partner(
|
||||
"Site Contact 2", "person", shipping_location
|
||||
)
|
||||
shipping_location.write(
|
||||
{
|
||||
"work_order_contacts": [
|
||||
Command.set([wo_contact_1.id, wo_contact_2.id])
|
||||
],
|
||||
"site_contacts": [Command.set([site_contact_1.id, site_contact_2.id])],
|
||||
}
|
||||
)
|
||||
|
||||
so = self._generate_sale_order(parent_co)
|
||||
|
||||
# Set back to a location without site or work order contacts
|
||||
form = Form(so)
|
||||
form.partner_shipping_id = parent_co
|
||||
form.save()
|
||||
# Make sure the contacts were reset on the SO
|
||||
self.assertFalse(so.work_order_contacts)
|
||||
self.assertFalse(so.site_contacts)
|
||||
|
||||
# Now set back to the location with the FSM contacts and make sure they get set
|
||||
# on the SO
|
||||
form.partner_shipping_id = shipping_location
|
||||
form.save()
|
||||
self.assertEqual(so.work_order_contacts, shipping_location.work_order_contacts)
|
||||
self.assertEqual(so.site_contacts, shipping_location.site_contacts)
|
||||
|
|
@ -1,144 +0,0 @@
|
|||
from odoo.tests import tagged
|
||||
from .test_bemade_fsm_common import BemadeFSMBaseTest
|
||||
from datetime import date, timedelta
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class FSMVisitTest(BemadeFSMBaseTest):
|
||||
def test_create_visit_sets_name_on_section(self):
|
||||
so = self._generate_sale_order()
|
||||
self._generate_sale_order_line(sale_order=so)
|
||||
|
||||
visit = self._generate_visit(so)
|
||||
|
||||
self.assertTrue(visit.so_section_id)
|
||||
self.assertEqual(visit.so_section_id.name, visit.label)
|
||||
|
||||
def test_change_visit_section_name(self):
|
||||
so = self._generate_sale_order()
|
||||
visit = self._generate_visit(so, label="First Label")
|
||||
line = visit.so_section_id
|
||||
|
||||
line.name = "Second Label"
|
||||
|
||||
self.assertEqual(visit.label, "Second Label")
|
||||
|
||||
def test_change_visit_label_changes_section_name(self):
|
||||
so = self._generate_sale_order()
|
||||
visit = self._generate_visit(so, label="First Label")
|
||||
line = visit.so_section_id
|
||||
|
||||
visit.label = "Second Label"
|
||||
|
||||
self.assertEqual(line.name, "Second Label")
|
||||
|
||||
def test_visit_completes_when_task_completes(self):
|
||||
so = self._generate_sale_order()
|
||||
visit = self._generate_visit(so)
|
||||
self._generate_sale_order_line(so)
|
||||
so.action_confirm()
|
||||
task = so.order_line.filtered(lambda line: line.task_id).task_id
|
||||
|
||||
task.action_fsm_validate()
|
||||
|
||||
self.assertTrue(visit.is_completed)
|
||||
|
||||
def test_visit_shows_invoiced_when_invoiced(self):
|
||||
so = self._generate_sale_order()
|
||||
visit = self._generate_visit(so)
|
||||
self._generate_sale_order_line(so)
|
||||
so.action_confirm()
|
||||
task = so.order_line.filtered(lambda line: line.task_id).task_id
|
||||
task.action_fsm_validate()
|
||||
|
||||
self._invoice_sale_order(so)
|
||||
|
||||
self.assertTrue(visit.is_invoiced)
|
||||
|
||||
def test_visit_groups_section_tasks_when_confirmed(self):
|
||||
so, visit, sol1, sol2 = self._generate_so_with_one_visit_two_lines()
|
||||
|
||||
so.action_confirm()
|
||||
|
||||
visit_task = visit.task_id
|
||||
self.assertTrue(visit_task)
|
||||
visit_subtasks = visit_task.child_ids
|
||||
self.assertTrue(
|
||||
visit_subtasks
|
||||
and sol1.task_id in visit_subtasks
|
||||
and sol2.task_id in visit_subtasks
|
||||
)
|
||||
|
||||
def test_visit_task_gets_correct_due_date_on_confirmation(self):
|
||||
so, visit, sol1, sol2 = self._generate_so_with_one_visit_two_lines()
|
||||
|
||||
so.action_confirm()
|
||||
|
||||
visit_task = visit.task_id
|
||||
self.assertEqual(visit_task.date_deadline, visit.approx_date)
|
||||
|
||||
def test_visit_task_gets_correct_duration_on_confirmation(self):
|
||||
partner = self._generate_partner()
|
||||
so = self._generate_sale_order(partner=partner)
|
||||
visit = self._generate_visit(sale_order=so)
|
||||
sol1 = self._generate_sale_order_line(sale_order=so, qty=4.0)
|
||||
sol2 = self._generate_sale_order_line(sale_order=so, qty=4.0)
|
||||
visit.approx_date = date.today() + timedelta(days=7)
|
||||
visit.so_section_id.sequence = 1
|
||||
sol1.sequence = 2
|
||||
sol2.sequence = 3
|
||||
|
||||
so.action_confirm()
|
||||
|
||||
visit_task = visit.task_id
|
||||
self.assertEqual(sol1.task_id.allocated_hours, 4.0)
|
||||
self.assertEqual(sol2.task_id.allocated_hours, 4.0)
|
||||
self.assertEqual(visit_task.allocated_hours, 8.0)
|
||||
|
||||
def test_adding_visit_creates_one_sale_order_line(self):
|
||||
self._generate_partner()
|
||||
so = self._generate_sale_order()
|
||||
self._generate_sale_order_line(sale_order=so)
|
||||
self._generate_sale_order_line(sale_order=so)
|
||||
|
||||
self._generate_visit(sale_order=so)
|
||||
|
||||
self.assertEqual(len(so.order_line), 3)
|
||||
|
||||
def test_marking_visit_task_done_completes_descendants(self):
|
||||
(
|
||||
so,
|
||||
visit,
|
||||
sol1,
|
||||
sol2,
|
||||
) = self._generate_so_with_one_visit_two_lines_and_descendants()
|
||||
so.action_confirm()
|
||||
parent = visit.task_id
|
||||
|
||||
parent.action_fsm_validate()
|
||||
|
||||
self._assert_is_done(parent)
|
||||
|
||||
def _assert_is_done(self, task):
|
||||
"""Recursively assert all tasks in a hierarchy are complete"""
|
||||
self.assertTrue(task.is_closed)
|
||||
for child in task.child_ids:
|
||||
self._assert_is_done(child)
|
||||
|
||||
def test_marking_visit_task_done_does_not_create_sale_order_line(self):
|
||||
so, visit, sol1, sol2 = self._generate_so_with_one_visit_two_lines()
|
||||
so.action_confirm()
|
||||
|
||||
visit.task_id.action_fsm_validate()
|
||||
|
||||
self.assertEqual(len(so.order_line), 3)
|
||||
|
||||
def test_confirming_so_names_visit_properly(self):
|
||||
"""Visits should be named <SO NUMBER> - Visit <visit #> - <visit label>"""
|
||||
so, visit, sol1, sol2 = self._generate_so_with_one_visit_two_lines()
|
||||
so.name = "SO12345"
|
||||
so.action_confirm()
|
||||
task = visit.task_id
|
||||
|
||||
supposed_name = "SVR12345-1 - Test Company - Test Label"
|
||||
self.assertEqual(task.name, supposed_name)
|
||||
|
|
@ -1,363 +0,0 @@
|
|||
from .test_task_template import BemadeFSMBaseTest
|
||||
from odoo.tests.common import tagged, Form
|
||||
from odoo import Command
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class TestSalesOrder(BemadeFSMBaseTest):
|
||||
@tagged("-at_install", "post_install")
|
||||
def test_order_confirmation_simple_template(self):
|
||||
"""Confirming the order should create a task in the global project based on the
|
||||
task template."""
|
||||
partner = self._generate_partner()
|
||||
so = self._generate_sale_order(partner=partner)
|
||||
task_template = self._generate_task_template(planned_hours=8)
|
||||
product = self._generate_product(task_template=task_template)
|
||||
sol = self._generate_sale_order_line(so, product=product)
|
||||
|
||||
so.action_confirm()
|
||||
|
||||
task = sol.task_id
|
||||
self.assertTrue(task)
|
||||
self.assertTrue(task_template.name in task.name)
|
||||
self.assertTrue(task_template.planned_hours == task.allocated_hours)
|
||||
|
||||
def test_task_template_tree_order_confirmation(self):
|
||||
partner = self._generate_partner()
|
||||
so = self._generate_sale_order(partner=partner)
|
||||
parent_template = self._generate_task_template(
|
||||
structure=[2, 1],
|
||||
names=["Parent Template", "Child Template", "Grandchild Template"],
|
||||
)
|
||||
child_template_1 = parent_template.subtasks[0]
|
||||
child_template_2 = parent_template.subtasks[1]
|
||||
grandchild_template = parent_template.subtasks[0].subtasks[0]
|
||||
product = self._generate_product(task_template=parent_template)
|
||||
sol = self._generate_sale_order_line(so, product=product)
|
||||
|
||||
so.action_confirm()
|
||||
|
||||
parent = sol.task_id
|
||||
c1, c2 = parent.child_ids
|
||||
gc = c1.child_ids[:1]
|
||||
self.assertTrue(parent.child_ids and len(parent.child_ids) == 2)
|
||||
self.assertTrue(parent_template.name in parent.name)
|
||||
self.assertEqual(child_template_1.name, c1.name)
|
||||
self.assertEqual(child_template_2.name, c2.name)
|
||||
self.assertTrue(c1.child_ids and len(c1.child_ids) == 1)
|
||||
self.assertEqual(grandchild_template.name, gc.name)
|
||||
|
||||
def test_order_confirmation_single_equipment(self):
|
||||
"""The equipment selected on the SO should transfer to the task."""
|
||||
partner = self._generate_partner()
|
||||
equipment = self._generate_equipment(partner=partner)
|
||||
so = self._generate_sale_order(partner=partner, equipment=equipment)
|
||||
task_template = self._generate_task_template(planned_hours=8)
|
||||
product1 = self._generate_product(task_template=task_template)
|
||||
product2 = self._generate_product()
|
||||
sol1 = self._generate_sale_order_line(so, product=product1)
|
||||
sol2 = self._generate_sale_order_line(so, product=product2)
|
||||
|
||||
so.action_confirm()
|
||||
|
||||
task1 = sol1.task_id
|
||||
task2 = sol2.task_id
|
||||
self.assertEqual(task1.equipment_ids[0], equipment)
|
||||
self.assertEqual(task2.equipment_ids[0], equipment)
|
||||
|
||||
def test_order_confirmation_multiple_equipment(self):
|
||||
"""All equipment items should flow from the sale order line to the final task"""
|
||||
partner = self._generate_partner()
|
||||
for i in range(5):
|
||||
self._generate_equipment(partner=partner)
|
||||
sale_order = self._generate_sale_order(
|
||||
partner=partner
|
||||
) # No default equipment since more than 3 on partner
|
||||
sol1, sol2, sol3 = [
|
||||
self._generate_sale_order_line(sale_order=sale_order) for _ in range(3)
|
||||
]
|
||||
sol1.equipment_ids = [
|
||||
Command.set([partner.equipment_ids[i].id for i in range(2)])
|
||||
]
|
||||
sol3.equipment_ids = [
|
||||
Command.set([partner.equipment_ids[i].id for i in range(2, 5)])
|
||||
]
|
||||
|
||||
sale_order.action_confirm()
|
||||
|
||||
self.assertEqual(sol1.equipment_ids, sol1.task_id.equipment_ids)
|
||||
self.assertEqual(sol2.equipment_ids, sol2.task_id.equipment_ids)
|
||||
self.assertEqual(sol3.equipment_ids, sol3.task_id.equipment_ids)
|
||||
|
||||
def test_task_template_with_equipment_flow(self):
|
||||
"""The equipment selected on a task template should flow down to the task
|
||||
created on SO confirmation."""
|
||||
partner = self._generate_partner()
|
||||
equipment = self._generate_equipment(partner=partner)
|
||||
so = self._generate_sale_order(partner=partner)
|
||||
task_template = self._generate_task_template(equipment=equipment)
|
||||
product = self._generate_product(task_template=task_template)
|
||||
sol = self._generate_sale_order_line(so, product=product)
|
||||
|
||||
so.action_confirm()
|
||||
|
||||
self.assertEqual(sol.task_id.equipment_ids[0], equipment)
|
||||
|
||||
def test_sale_order_line_gets_default_equipment(self):
|
||||
"""Sale order lines created on a SO with default equipment set should inherit
|
||||
that default equipment."""
|
||||
partner = self._generate_partner()
|
||||
self._generate_equipment(partner=partner)
|
||||
sale_order = self._generate_sale_order(partner=partner)
|
||||
|
||||
sol = self._generate_sale_order_line(sale_order=sale_order)
|
||||
|
||||
self.assertEqual(sol.equipment_ids, partner.equipment_ids)
|
||||
|
||||
def test_sale_order_gets_correct_default_equipment_from_partner(self):
|
||||
"""Should pick up equipment from the partner."""
|
||||
partner = self._generate_partner()
|
||||
self._generate_equipment(partner=partner)
|
||||
|
||||
sale_order = self._generate_sale_order(partner=partner)
|
||||
|
||||
self.assertEqual(sale_order.default_equipment_ids, partner.owned_equipment_ids)
|
||||
|
||||
def test_sale_order_default_equipment_maximum_number(self):
|
||||
parent = self._generate_partner()
|
||||
child = self._generate_partner(parent=parent)
|
||||
for i in range(3):
|
||||
self._generate_equipment(child)
|
||||
|
||||
sale_order = self._generate_sale_order(partner=parent)
|
||||
|
||||
self.assertEqual(sale_order.default_equipment_ids, parent.owned_equipment_ids)
|
||||
|
||||
def test_sale_order_no_default_equipment_with_more_than_three_owned_on_partner(
|
||||
self,
|
||||
):
|
||||
parent = self._generate_partner()
|
||||
child = self._generate_partner(parent=parent)
|
||||
for i in range(4):
|
||||
self._generate_equipment(child)
|
||||
|
||||
sale_order = self._generate_sale_order(partner=parent)
|
||||
|
||||
self.assertEqual(sale_order.default_equipment_ids, parent.owned_equipment_ids)
|
||||
|
||||
def test_sale_order_resets_default_equipment_on_partner_change(self):
|
||||
partner_1 = self._generate_partner()
|
||||
partner_2 = self._generate_partner()
|
||||
self._generate_equipment(partner=partner_1)
|
||||
sale_order = self._generate_sale_order(partner_1)
|
||||
form = Form(sale_order)
|
||||
|
||||
form.partner_id = partner_2
|
||||
form.save()
|
||||
|
||||
self.assertFalse(sale_order.default_equipment_ids)
|
||||
|
||||
def test_sale_order_prioritize_shipping_location_equipments(self):
|
||||
parent = self._generate_partner()
|
||||
child = self._generate_partner(parent=parent, location_type="delivery")
|
||||
self._generate_equipment(partner=parent)
|
||||
self._generate_equipment(partner=child)
|
||||
|
||||
sale_order = self._generate_sale_order(partner=parent, shipping_location=child)
|
||||
|
||||
self.assertEqual(sale_order.default_equipment_ids, child.equipment_ids)
|
||||
|
||||
def test_default_equipment_transfers_to_sale_order_line(self):
|
||||
partner = self._generate_partner()
|
||||
for i in range(3):
|
||||
self._generate_equipment(partner=partner)
|
||||
sale_order = self._generate_sale_order(partner=partner)
|
||||
|
||||
for i in range(3):
|
||||
self._generate_sale_order_line(sale_order=sale_order)
|
||||
|
||||
for line in sale_order.order_line:
|
||||
self.assertEqual(line.equipment_ids, partner.equipment_ids)
|
||||
|
||||
def test_task_mark_done(self):
|
||||
"""Marking the task linked to a SO line should mark the line delivered.
|
||||
Marking sub-tasks done should not."""
|
||||
partner = self._generate_partner()
|
||||
so = self._generate_sale_order(partner=partner)
|
||||
task_template = self._generate_task_template(
|
||||
structure=[2], names=["Parent Task", "Subtask"]
|
||||
)
|
||||
product = self._generate_product(task_template=task_template)
|
||||
sol = self._generate_sale_order_line(so, product=product)
|
||||
so.action_confirm()
|
||||
parent_task = sol.task_id
|
||||
subtasks = parent_task._get_all_subtasks()
|
||||
|
||||
# Marking the subtasks done should not increment delivered quantity
|
||||
subtasks.action_fsm_validate(True)
|
||||
self.assertEqual(sol.qty_delivered, 0)
|
||||
|
||||
# Marking the top-level tasks done should set the delivered quantity to some
|
||||
# non-zero value based on the UOM
|
||||
parent_task.action_fsm_validate(True)
|
||||
self.assertTrue(sol.qty_delivered != 0)
|
||||
|
||||
def test_task_contacts_through_sale_order(self):
|
||||
"""Make sure the site contacts and work order contacts transfer correctly
|
||||
from the SO to the task."""
|
||||
|
||||
partner = self._generate_partner()
|
||||
contact1 = self._generate_partner("Site contact", "person", partner)
|
||||
contact2 = self._generate_partner("Work order contact", "person", partner)
|
||||
partner.write(
|
||||
{
|
||||
"site_contacts": [Command.set([contact1.id])],
|
||||
"work_order_contacts": [Command.set([contact2.id])],
|
||||
}
|
||||
)
|
||||
so = self._generate_sale_order(partner)
|
||||
product = self._generate_product()
|
||||
sol = self._generate_sale_order_line(sale_order=so, product=product)
|
||||
|
||||
so.action_confirm()
|
||||
|
||||
self.assertEqual(so.work_order_contacts, partner.work_order_contacts)
|
||||
self.assertEqual(so.site_contacts, partner.site_contacts)
|
||||
self.assertEqual(sol.task_id.work_order_contacts, partner.work_order_contacts)
|
||||
self.assertEqual(sol.task_id.site_contacts, partner.site_contacts)
|
||||
|
||||
def test_tasks_created_at_order_confirmation_have_no_assignees(self):
|
||||
so, visit, sol1, sol2 = self._generate_so_with_one_visit_two_lines()
|
||||
user = self._generate_project_user(name="User", login="login")
|
||||
|
||||
# We test as a specific user since testing as root may not produce the error
|
||||
so.with_user(user).action_confirm()
|
||||
|
||||
visit_task = visit.task_id
|
||||
subtask1 = visit_task.child_ids[0]
|
||||
subtask2 = visit_task.child_ids[1]
|
||||
self.assertFalse(visit_task.user_ids)
|
||||
self.assertFalse(subtask1.user_ids)
|
||||
self.assertFalse(subtask2.user_ids)
|
||||
|
||||
def test_long_line_name_overflows_to_task_description(self):
|
||||
so = self._generate_sale_order()
|
||||
product = self._generate_product()
|
||||
product.description_sale = (
|
||||
"This is a long product description.\n"
|
||||
"It even spans multiple lines.\n"
|
||||
"One could find this annoying in a task name."
|
||||
)
|
||||
|
||||
sol = self._generate_sale_order_line(sale_order=so, product=product)
|
||||
|
||||
so.action_confirm()
|
||||
task = sol.task_id
|
||||
|
||||
self.assertFalse("This is a long product description." in task.name)
|
||||
self.assertFalse("It even spans multiple lines." in task.name)
|
||||
self.assertFalse("One could find this annoying in a task name." in task.name)
|
||||
self.assertTrue("It even spans multiple lines." in task.description)
|
||||
self.assertTrue(
|
||||
"One could find this annoying in a task name." in task.description
|
||||
)
|
||||
|
||||
def test_subtask_templates_no_description_if_blank_on_template(self):
|
||||
so = self._generate_sale_order()
|
||||
template = self._generate_task_template(
|
||||
structure=[5], names=["Parent", "Child"]
|
||||
)
|
||||
template.description = ""
|
||||
template.subtasks[0].description = "Some fixed description"
|
||||
for t in template.subtasks[1:]:
|
||||
t.description = ""
|
||||
product = self._generate_product(task_template=template)
|
||||
sol = self._generate_sale_order_line(sale_order=so, product=product)
|
||||
|
||||
so.action_confirm()
|
||||
|
||||
task = sol.task_id
|
||||
self.assertEqual(
|
||||
task.child_ids[0].description, template.subtasks[0].description
|
||||
)
|
||||
for t in task.child_ids[1:]:
|
||||
self.assertFalse(t.description)
|
||||
|
||||
def test_duplicate_sale_order_duplicates_visits(self):
|
||||
"""Duplicated sales orders should have visits tied to their SO lines as in the
|
||||
original. The copied visits should not have approximate dates set, however."""
|
||||
so, visit, line1, line2 = self._generate_so_with_one_visit_two_lines()
|
||||
|
||||
so2 = so.copy()
|
||||
|
||||
self.assertTrue(so2.visit_ids)
|
||||
visit2 = so2.visit_ids[0]
|
||||
self.assertEqual(so2.order_line[0].visit_id, visit2)
|
||||
self.assertEqual(visit2.label, visit.label)
|
||||
self.assertFalse(visit2.approx_date)
|
||||
|
||||
def test_confirming_sale_order_creates_visit_if_none_created(self):
|
||||
so = self._generate_sale_order()
|
||||
so.company_id.create_default_fsm_visit = True
|
||||
product = self._generate_product()
|
||||
self._generate_sale_order_line(so, product)
|
||||
|
||||
so.action_confirm()
|
||||
|
||||
visit_line = so.order_line.sorted("sequence")[0]
|
||||
self.assertTrue(so.visit_ids)
|
||||
self.assertEqual(visit_line.visit_id, so.visit_ids)
|
||||
|
||||
def test_confirming_sale_order_with_visit_creates_no_new_lines(self):
|
||||
so = self._generate_sale_order()
|
||||
so.company_id.create_default_fsm_visit = True
|
||||
self._generate_product()
|
||||
self._generate_visit(so)
|
||||
|
||||
so.action_confirm()
|
||||
|
||||
self.assertEqual(len(so.visit_ids), 1)
|
||||
|
||||
def test_confirming_sale_order_creates_no_visit_if_setting_off(self):
|
||||
so = self._generate_sale_order()
|
||||
so.company_id.create_default_fsm_visit = False
|
||||
product = self._generate_product()
|
||||
self._generate_sale_order_line(so, product)
|
||||
|
||||
so.action_confirm()
|
||||
|
||||
self.assertFalse(so.visit_ids)
|
||||
|
||||
def test_changing_sale_order_customer_follows_to_tasks_and_subtasks(self):
|
||||
so = self._generate_sale_order()
|
||||
so = self._generate_sale_order()
|
||||
so.company_id.create_default_fsm_visit = False
|
||||
task_template = self._generate_task_template(
|
||||
parent=None,
|
||||
structure=[2, 2],
|
||||
names=["Parent", "Child", "Grandchild"],
|
||||
)
|
||||
product = self._generate_product(task_template=task_template)
|
||||
sol = self._generate_sale_order_line(so, product)
|
||||
|
||||
so.action_confirm()
|
||||
|
||||
parent_task = sol.task_id
|
||||
for task in parent_task._get_all_subtasks() | parent_task:
|
||||
self.assertEqual(so.partner_shipping_id, task.partner_id)
|
||||
|
||||
so.write(
|
||||
{
|
||||
"partner_shipping_id": self.env["res.partner"]
|
||||
.create(
|
||||
{
|
||||
"name": "New shipping address",
|
||||
"parent_id": so.partner_id.id,
|
||||
"type": "delivery",
|
||||
}
|
||||
)
|
||||
.id
|
||||
}
|
||||
)
|
||||
for task in parent_task._get_all_subtasks() | parent_task:
|
||||
self.assertEqual(so.partner_shipping_id, task.partner_id)
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
from odoo.tests import TransactionCase, Form, tagged
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class TestSettings(TransactionCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.test_partner_co = cls.env["res.partner"].create(
|
||||
{
|
||||
"name": "Test Co",
|
||||
}
|
||||
)
|
||||
cls.test_co = cls.env["res.company"].create(
|
||||
{
|
||||
"name": "Test Co",
|
||||
"country_id": cls.env.ref("base.ca").id,
|
||||
}
|
||||
)
|
||||
cls.env.user.company_id = cls.test_co
|
||||
|
||||
def test_enabling_separate_time_on_work_orders(self):
|
||||
wizard = self.env["res.config.settings"].create({})
|
||||
self.assertFalse(self.test_co.split_time_from_materials_on_service_work_orders)
|
||||
with Form(wizard) as form:
|
||||
form.separate_time_on_work_orders = True
|
||||
self.assertTrue(self.test_co.split_time_from_materials_on_service_work_orders)
|
||||
|
||||
def test_disabling_separate_time_on_work_orders(self):
|
||||
wizard = self.env["res.config.settings"].create({})
|
||||
self.test_co.split_time_from_materials_on_service_work_orders = True
|
||||
with Form(wizard) as form:
|
||||
form.separate_time_on_work_orders = False
|
||||
self.assertFalse(self.test_co.split_time_from_materials_on_service_work_orders)
|
||||
|
||||
def test_enabling_create_default_fsm_visit(self):
|
||||
wizard = self.env["res.config.settings"].create({})
|
||||
self.test_co.create_default_fsm_visit = False
|
||||
with Form(wizard) as form:
|
||||
form.create_default_fsm_visit = True
|
||||
self.assertTrue(self.test_co.create_default_fsm_visit)
|
||||
|
||||
def test_disabling_create_default_fsm_visit(self):
|
||||
wizard = self.env["res.config.settings"].create({})
|
||||
self.test_co.create_default_fsm_visit = True
|
||||
with Form(wizard) as form:
|
||||
form.create_default_fsm_visit = False
|
||||
self.assertFalse(self.test_co.create_default_fsm_visit)
|
||||
|
|
@ -1,154 +0,0 @@
|
|||
from .test_bemade_fsm_common import BemadeFSMBaseTest
|
||||
from odoo.tests.common import tagged, Form
|
||||
from odoo import Command
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TaskTest(BemadeFSMBaseTest):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
# Chose to set up all tests the same way since this code was becoming very
|
||||
# redundant
|
||||
super().setUpClass()
|
||||
cls.user = cls._generate_project_manager_user("Bob", "Bob")
|
||||
|
||||
def _generate_so_with_multilevel_task_template(self):
|
||||
so = self._generate_sale_order()
|
||||
template = self._generate_task_template(
|
||||
names=["Parent", "Child", "Grandchild"], structure=[2, 1]
|
||||
)
|
||||
product = self._generate_product(task_template=template)
|
||||
sol = self._generate_sale_order_line(sale_order=so, product=product)
|
||||
return so, sol
|
||||
|
||||
def test_reassigning_assignment_propagating_task_changes_subtasks(self):
|
||||
so, sol = self._generate_so_with_multilevel_task_template()
|
||||
so.action_confirm()
|
||||
task = sol.task_id
|
||||
|
||||
task.propagate_assignment = True
|
||||
task.write(
|
||||
{
|
||||
"user_ids": [Command.set([self.user.id])],
|
||||
"propagate_assignment": True,
|
||||
}
|
||||
)
|
||||
|
||||
self.assertTrue(
|
||||
all([t.user_ids == self.user for t in task | task._get_all_subtasks()])
|
||||
)
|
||||
|
||||
def test_reassigning_task_doesnt_propagate_by_default(self):
|
||||
so, sol = self._generate_so_with_multilevel_task_template()
|
||||
so.action_confirm()
|
||||
task = sol.task_id
|
||||
|
||||
task.write(
|
||||
{
|
||||
"user_ids": [Command.set([self.user.id])],
|
||||
}
|
||||
)
|
||||
|
||||
self.assertFalse(any([t.user_ids for t in task.child_ids.child_ids]))
|
||||
|
||||
def test_unset_propagate_assignment_unsets_for_all_children(self):
|
||||
so, sol = self._generate_so_with_multilevel_task_template()
|
||||
so.action_confirm()
|
||||
task = sol.task_id
|
||||
# First, set propagation and assign
|
||||
task.propagate_assignment = True
|
||||
task.write({"user_ids": [Command.set([self.user.id])]})
|
||||
# Then, unset propagation for the children and re-set assignment
|
||||
task.child_ids.write({"propagate_assignment": False})
|
||||
self.assertFalse(
|
||||
any([t.propagate_assignment for t in task._get_all_subtasks()])
|
||||
)
|
||||
# Then, test that assigning the parent only assigns its children, not its
|
||||
# grandchildren
|
||||
task.write({"user_ids": [Command.set([])]})
|
||||
self.assertTrue(all([not t.user_ids for t in task | task.child_ids]))
|
||||
self.assertTrue(
|
||||
all([t.user_ids == self.user for t in task.child_ids.child_ids])
|
||||
)
|
||||
|
||||
def test_task_gets_work_order_contacts_from_sale_order(self):
|
||||
so, sol = self._generate_so_with_multilevel_task_template()
|
||||
work_order_contacts = self._generate_partner(
|
||||
parent=so.partner_id
|
||||
) | self._generate_partner(parent=so.partner_id)
|
||||
so.write({"work_order_contacts": [(6, 0, work_order_contacts.ids)]})
|
||||
|
||||
so.action_confirm()
|
||||
task = sol.task_id
|
||||
|
||||
self.assertEqual(task.work_order_contacts, so.work_order_contacts)
|
||||
# Just a safeguard to make sure we set it properly on the SO
|
||||
self.assertEqual(len(task.work_order_contacts), 2)
|
||||
# Make sure all subtasks got the same
|
||||
for subtask in task._get_all_subtasks():
|
||||
self.assertEqual(subtask.work_order_contacts, so.work_order_contacts)
|
||||
|
||||
def test_task_gets_site_contacts_from_sale_order(self):
|
||||
so, sol = self._generate_so_with_multilevel_task_template()
|
||||
site_contacts = self._generate_partner(
|
||||
parent=so.partner_id
|
||||
) | self._generate_partner(parent=so.partner_id)
|
||||
so.write({"site_contacts": [(6, 0, site_contacts.ids)]})
|
||||
|
||||
so.action_confirm()
|
||||
task = sol.task_id
|
||||
|
||||
self.assertEqual(task.site_contacts, so.site_contacts)
|
||||
# Just a safeguard to make sure we set it properly on the SO
|
||||
self.assertEqual(len(task.site_contacts), 2)
|
||||
# Make sure all subtasks got the same
|
||||
for subtask in task._get_all_subtasks():
|
||||
self.assertEqual(subtask.site_contacts, so.site_contacts)
|
||||
|
||||
def test_task_gets_work_order_contacts_from_parent(self):
|
||||
so, sol = self._generate_so_with_multilevel_task_template()
|
||||
work_order_contacts = self._generate_partner(
|
||||
parent=so.partner_id
|
||||
) | self._generate_partner(parent=so.partner_id)
|
||||
so.write({"work_order_contacts": [(6, 0, work_order_contacts.ids)]})
|
||||
|
||||
so.action_confirm()
|
||||
task = sol.task_id
|
||||
task.write(
|
||||
{
|
||||
"work_order_contacts": [
|
||||
Command.link(self._generate_partner(parent=so.partner_id).id)
|
||||
]
|
||||
}
|
||||
)
|
||||
for subtask in task._get_all_subtasks():
|
||||
self.assertEqual(subtask.work_order_contacts, task.work_order_contacts)
|
||||
with Form(task) as task_form:
|
||||
with task_form.child_ids.new() as subtask:
|
||||
subtask.name = "Subtask 1"
|
||||
subtask = task.child_ids[-1]
|
||||
self.assertEqual(subtask.work_order_contacts, task.work_order_contacts)
|
||||
|
||||
def test_task_gets_site_contacts_from_parent(self):
|
||||
so, sol = self._generate_so_with_multilevel_task_template()
|
||||
site_contacts = self._generate_partner(
|
||||
parent=so.partner_id
|
||||
) | self._generate_partner(parent=so.partner_id)
|
||||
so.write({"site_contacts": [(6, 0, site_contacts.ids)]})
|
||||
|
||||
so.action_confirm()
|
||||
task = sol.task_id
|
||||
task.write(
|
||||
{
|
||||
"site_contacts": [
|
||||
Command.link(self._generate_partner(parent=so.partner_id).id)
|
||||
]
|
||||
}
|
||||
)
|
||||
for subtask in task._get_all_subtasks():
|
||||
self.assertEqual(subtask.site_contacts, task.site_contacts)
|
||||
with Form(task) as task_form:
|
||||
with task_form.child_ids.new() as subtask:
|
||||
subtask.name = "Subtask 1"
|
||||
subtask = task.child_ids[-1]
|
||||
self.assertEqual(subtask.site_contacts, task.site_contacts)
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
from .test_bemade_fsm_common import BemadeFSMBaseTest
|
||||
from odoo.tests import Form
|
||||
|
||||
|
||||
class TestTaskReport(BemadeFSMBaseTest):
|
||||
def test_split_time_materials_setting(self):
|
||||
with Form(self.env["res.config.settings"]) as settings:
|
||||
settings.separate_time_on_work_orders = True
|
||||
|
||||
with Form(self.env["res.config.settings"]):
|
||||
self.assertTrue(settings.separate_time_on_work_orders)
|
||||
|
||||
so = self._generate_sale_order()
|
||||
service_product = self._generate_product()
|
||||
material_product = self._generate_product(
|
||||
name="Material Product",
|
||||
product_type="product",
|
||||
service_tracking="no",
|
||||
)
|
||||
visit = self._generate_visit(sale_order=so)
|
||||
self._generate_sale_order_line(sale_order=so, product=service_product)
|
||||
self._generate_sale_order_line(sale_order=so, product=material_product)
|
||||
so.action_confirm()
|
||||
task = visit.task_id
|
||||
|
||||
html_content = (
|
||||
self.env["ir.actions.report"]
|
||||
._render(
|
||||
"industry_fsm_report.worksheet_custom",
|
||||
[task.id],
|
||||
)[0]
|
||||
.decode("utf-8")
|
||||
.split("\n")
|
||||
)
|
||||
|
||||
strings_to_find = ["<h2>Materials</h2>", "<span>Material Product</span>"]
|
||||
|
||||
for line in strings_to_find:
|
||||
line_found = False
|
||||
for html_line in html_content:
|
||||
if line in html_line:
|
||||
line_found = True
|
||||
self.assertTrue(line_found, f"{line} should be in file.")
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
from .test_bemade_fsm_common import BemadeFSMBaseTest
|
||||
from odoo.tests.common import tagged, Form
|
||||
from odoo.exceptions import MissingError
|
||||
from odoo.tools import mute_logger
|
||||
import psycopg2
|
||||
|
||||
|
||||
@tagged("-at_install", "post_install")
|
||||
class TestTaskTemplate(BemadeFSMBaseTest):
|
||||
def test_delete_task_template(self):
|
||||
"""User should never be able to delete a task template used on a product"""
|
||||
task_template = self._generate_task_template(names=["Template 1"])
|
||||
self._generate_product(name="Test Product 1", task_template=task_template)
|
||||
with self.assertRaises(psycopg2.errors.ForeignKeyViolation):
|
||||
with mute_logger("odoo.sql_db"):
|
||||
task_template.unlink()
|
||||
|
||||
def test_delete_subtask_template(self):
|
||||
"""Deletion of a child task should be OK even if the parent is on a product.
|
||||
Children of the deleted subtask should be deleted."""
|
||||
parent_task = self._generate_task_template(
|
||||
structure=[2, 1],
|
||||
names=["Parent Template", "Child Template", "Grandchild Template"],
|
||||
)
|
||||
grandchild_task = parent_task.subtasks[0].subtasks[0]
|
||||
|
||||
parent_task.subtasks[0].unlink()
|
||||
|
||||
# Reading deleted child's name field should be impossible
|
||||
with self.assertRaises(MissingError):
|
||||
_ = grandchild_task.name
|
||||
|
||||
def test_dissociating_customer_resets_equipment_appropriately(self):
|
||||
partner1 = self._generate_partner()
|
||||
partner2 = self._generate_partner()
|
||||
equipment1 = self._generate_equipment(partner=partner1)
|
||||
task = self._generate_task_template(customer=partner1, equipment=equipment1)
|
||||
form = Form(task)
|
||||
|
||||
# Switching the partner should trigger on_change that makes sure equipments are
|
||||
# linked to the new partner
|
||||
form.customer = partner2
|
||||
form.save()
|
||||
|
||||
self.assertFalse(equipment1 in task.equipment_ids)
|
||||
|
||||
def test_child_task_names_are_short_version(self):
|
||||
so, visit, sol1, sol2 = self._generate_so_with_one_visit_two_lines()
|
||||
template = self._generate_task_template(names=["Task"])
|
||||
product = self._generate_product(task_template=template)
|
||||
sol1.name = "Short Name 1"
|
||||
sol2.name = "Short Name 2"
|
||||
sol3 = self._generate_sale_order_line(sale_order=so, product=product)
|
||||
|
||||
so.action_confirm()
|
||||
|
||||
self.assertEqual(sol1.task_id.name, "Short Name 1")
|
||||
self.assertEqual(sol2.task_id.name, "Short Name 2")
|
||||
self.assertEqual(sol3.task_id.name, "Task")
|
||||
|
||||
def test_task_creation_directly_from_template(self):
|
||||
project = self.env.ref("industry_fsm.fsm_project")
|
||||
template = self._generate_task_template(
|
||||
names=["Task", "Child", "Grandchild"], structure=[2, 1]
|
||||
)
|
||||
|
||||
task = template.create_task_from_self(project, "My new task")
|
||||
|
||||
self.assertEqual(len(task.child_ids), len(template.subtasks))
|
||||
self.assertEqual(
|
||||
len(task.child_ids[0].child_ids), len(template.subtasks[0].subtasks)
|
||||
)
|
||||
self.assertEqual(
|
||||
len(task.child_ids[1].child_ids), len(template.subtasks[1].subtasks)
|
||||
)
|
||||
self.assertEqual(task.name, "My new task")
|
||||
self.assertEqual(task.child_ids[0].name, template.subtasks[0].name)
|
||||
self.assertTrue(
|
||||
all([t.project_id == project for t in task | task._get_all_subtasks()])
|
||||
)
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<menuitem
|
||||
id="project_task_template_menu"
|
||||
name="Task Templates"
|
||||
parent="project.menu_main_pm"
|
||||
action="task_template_act_window"
|
||||
groups="project.group_project_manager,project.group_project_user"
|
||||
/>
|
||||
<menuitem
|
||||
id="service_task_template_meny"
|
||||
name="Task Templates"
|
||||
action="task_template_act_window"
|
||||
parent="industry_fsm.fsm_menu_root"
|
||||
groups="project.group_project_manager,industry_fsm.group_fsm_manager"
|
||||
/>
|
||||
</odoo>
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="product_template_form_inherit" model="ir.ui.view">
|
||||
<field name="name">bemade_fsm.product.template.form</field>
|
||||
<field name="model">product.template</field>
|
||||
<field
|
||||
name="inherit_id"
|
||||
ref="sale_project.product_template_form_view_invoice_policy_inherit_sale_project"
|
||||
/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='project_id']" position="after">
|
||||
<field
|
||||
name="task_template_id"
|
||||
invisible="service_tracking not in ('task_global_project', 'task_in_project')"
|
||||
domain="[('parent', '=', False)]"
|
||||
/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- BV: Did comment that one cause can not match inherit_id and don't understand it use -->
|
||||
<!-- <record id="product_search_form_view_inherit_bemade_fsm" model="ir.ui.view">-->
|
||||
<!-- <field name="name">bemade_fsm.product_search_form_view_inherit_bemade_fsm</field>-->
|
||||
<!-- <field name="model">product.product</field>-->
|
||||
<!-- <field name="inherit_id" ref="industry_fsm_sale.product_search_form_view_inherit_fsm_sale"/>-->
|
||||
<!-- <field name="arch" type="xml">-->
|
||||
<!-- <xpath expr="//searchpanel//field[@name='categ_id']" position="attributes">-->
|
||||
<!-- <attribute name="limit">0</attribute>-->
|
||||
<!-- </xpath>-->
|
||||
<!-- </field>-->
|
||||
<!-- </record>-->
|
||||
</odoo>
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
<?xml version="1.0" ?>
|
||||
<odoo>
|
||||
<record id="partner_equipment_location_view_form" model="ir.ui.view">
|
||||
<field name="name">partner.equipment.location.view.form</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field
|
||||
name="inherit_id"
|
||||
ref="fsm_equipment.partner_equipment_location_view_form"
|
||||
/>
|
||||
<field name="arch" type="xml">
|
||||
<page name="equipment" position="attributes">
|
||||
<attribute name="invisible">site_ids</attribute>
|
||||
</page>
|
||||
<page name="equipment" position="after">
|
||||
<page
|
||||
name="Service Contacts"
|
||||
invisible="site_ids">
|
||||
<field
|
||||
name="site_contacts"
|
||||
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"
|
||||
/>
|
||||
<field
|
||||
name="work_order_contacts"
|
||||
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"
|
||||
/>
|
||||
</page>
|
||||
<field name="is_service_site" invisible="True"/>
|
||||
<page name="Service Sites" invisible="is_service_site">
|
||||
<group>
|
||||
<field name="site_ids">
|
||||
<tree editable="bottom">
|
||||
<field name="name" widget="res_partner_many2one" />
|
||||
</tree>
|
||||
</field>
|
||||
</group>
|
||||
</page>
|
||||
</page>
|
||||
</field>
|
||||
</record>
|
||||
<record id="fsm_contacts_view_tree" model="ir.ui.view">
|
||||
<field name="name">bemade_fsm.contacts.tree</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree editable="bottom">
|
||||
<field name="name" />
|
||||
<field name="email" widget="email" />
|
||||
<field name="phone" widget="phone" />
|
||||
<field name="mobile" widget="phone" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="sale_order_form_inherit" model="ir.ui.view">
|
||||
<field name="name">bemade_fsm.sale_order.form</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="inherit_id" ref="sale.view_order_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//page[@name='other_information']" position="before">
|
||||
<page name="field_service" string="Field Service">
|
||||
<group name="fsm_visits" string="Service Visits">
|
||||
<field
|
||||
name="visit_ids"
|
||||
context="{'tree_view_ref': 'bemade_fsm.bemade_fsm_visit_tree'}"
|
||||
/>
|
||||
</group>
|
||||
<group name="field_service_info" string="Contacts and Equipment">
|
||||
<field name="valid_equipment_ids" invisible="1" />
|
||||
<field
|
||||
name="summary_equipment_ids"
|
||||
context="{'default_partner_id': partner_shipping_id,}"
|
||||
widget="many2many_tags"
|
||||
groups="account.group_delivery_invoice_address"
|
||||
/>
|
||||
<field
|
||||
name="site_contacts"
|
||||
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"
|
||||
/>
|
||||
<field
|
||||
name="work_order_contacts"
|
||||
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"
|
||||
/>
|
||||
|
||||
<field
|
||||
name="default_equipment_ids"
|
||||
context="{'default_partner_id': partner_shipping_id,}"
|
||||
widget="many2many_tags"
|
||||
groups="account.group_delivery_invoice_address"
|
||||
/>
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
<xpath expr="//tree//field[@name='name']" position="after">
|
||||
<field name="valid_equipment_ids" invisible="1" />
|
||||
<field
|
||||
name="equipment_ids"
|
||||
widget="many2many_tags"
|
||||
domain="[('id', 'in', valid_equipment_ids)]"
|
||||
/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<record id="bemade_fsm_visit_tree" model="ir.ui.view">
|
||||
<field name="name">bemade_fsm.visit.tree</field>
|
||||
<field name="model">bemade_fsm.visit</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree editable="bottom">
|
||||
<field name="label" />
|
||||
<field name="approx_date" />
|
||||
<field name="is_completed" widget="boolean" />
|
||||
<field name="is_invoiced" widget="boolean" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -1,127 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="task_template_form_view" model="ir.ui.view">
|
||||
<field name="name">bemade_fsm.task_template.form</field>
|
||||
<field name="model">project.task.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Task Template">
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<label for="name" />
|
||||
<h1>
|
||||
<field name="name" placeholder="Title" />
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="project" />
|
||||
<field name="assignees" widget="many2many_avatar_user" />
|
||||
<field name="parent" />
|
||||
<field name="planned_hours" />
|
||||
</group>
|
||||
<group>
|
||||
<field name="customer" />
|
||||
<field
|
||||
name="equipment_ids"
|
||||
domain="[('partner_id', '=', customer)]"
|
||||
context="{'tree_view_ref': 'fsm_equipment.equipment_view_tree'}"
|
||||
/>
|
||||
<field name="tags" widget="many2many_tags" />
|
||||
<field name="company_id" />
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page name="description_page" string="Description">
|
||||
<field
|
||||
name="description"
|
||||
type="html"
|
||||
options="{'collaborative': true}"
|
||||
/>
|
||||
</page>
|
||||
<page name="subtasks_page" string="Subtasks">
|
||||
<field name="subtasks">
|
||||
<tree editable="bottom">
|
||||
<field name="sequence" widget="handle" />
|
||||
<field name="name" />
|
||||
<field name="customer" />
|
||||
<field
|
||||
name="assignees"
|
||||
widget="many2many_avatar_user"
|
||||
/>
|
||||
<button
|
||||
name="action_open_task"
|
||||
type="object"
|
||||
title="View Task"
|
||||
string="View Task"
|
||||
class="btn btn-link pull-right"
|
||||
/>
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="task_template_tree_view" model="ir.ui.view">
|
||||
<field name="name">project.task_template.tree</field>
|
||||
<field name="model">project.task.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="name" />
|
||||
<field name="assignees" widget="many2many_avatar_user" />
|
||||
<field name="project" />
|
||||
<field name="parent" />
|
||||
<field name="planned_hours" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="task_template_search_view" model="ir.ui.view">
|
||||
<field name="name">project.task_template.search</field>
|
||||
<field name="model">project.task.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Task Template">
|
||||
<field name="name" />
|
||||
<field name="project" />
|
||||
<field name="assignees" />
|
||||
<field name="parent" />
|
||||
<field name="subtasks" />
|
||||
<field name="planned_hours" />
|
||||
<group expand="1" string="Group By">
|
||||
<filter
|
||||
string="Project"
|
||||
name="groupby_project"
|
||||
domain="[]"
|
||||
context="{'group_by':'project'}"
|
||||
/>
|
||||
<filter
|
||||
string="Parent Task"
|
||||
name="groupby_parent"
|
||||
domain="[]"
|
||||
context="{'group_by':'parent'}"
|
||||
/>
|
||||
<filter
|
||||
string="Customer"
|
||||
name="groupby_customer"
|
||||
domain="[]"
|
||||
context="{'group_by':'customer'}"
|
||||
/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="task_template_act_window" model="ir.actions.act_window">
|
||||
<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="help" type="html">
|
||||
<p class="oe_view_nocontent_create">
|
||||
There are no task templates, click above to create one.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -1,229 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<record id="bemade_fsm_project_task_form_inherit" model="ir.ui.view">
|
||||
<field name="name">bemade_fsm.project_task.form</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="inherit_id" ref="industry_fsm.view_task_form2_inherit" />
|
||||
<field name="priority" eval="8" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='name']/.." position="before">
|
||||
<h1 class="d-flex flex-row justify-content-between">
|
||||
<field
|
||||
name="work_order_number"
|
||||
invisible="work_order_number == False"
|
||||
/>
|
||||
</h1>
|
||||
</xpath>
|
||||
<xpath expr="//page[@name='extra_info']" position="after">
|
||||
<page string="Equipment and Contacts" name="equipment_contacts"
|
||||
translate="True">
|
||||
<group name="equipment_and_contacts">
|
||||
<field
|
||||
name="equipment_ids"
|
||||
domain="[('partner_id', '=', partner_id)]"
|
||||
context="{'tree_view_ref': 'fsm_equipment.equipment_view_tree'}"
|
||||
/>
|
||||
<field
|
||||
name="site_contacts"
|
||||
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"
|
||||
/>
|
||||
<field
|
||||
name="work_order_contacts"
|
||||
context="{'tree_view_ref': 'bemade_fsm.fsm_contacts_view_tree'}"
|
||||
/>
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
<button
|
||||
name="action_fsm_validate"
|
||||
class='btn-primary'
|
||||
position="attributes"
|
||||
>
|
||||
<attribute name="string">Mark as Delivered</attribute>
|
||||
<attribute name="groups">industry_fsm.group_fsm_manager</attribute>
|
||||
</button>
|
||||
<button
|
||||
name="action_fsm_validate"
|
||||
class='btn-secondary'
|
||||
position="attributes"
|
||||
>
|
||||
<attribute name="string">Mark as Delivered</attribute>
|
||||
<attribute name="groups">industry_fsm.group_fsm_manager</attribute>
|
||||
</button>
|
||||
</field>
|
||||
</record>
|
||||
<record id="view_task_form2_inherit" model="ir.ui.view">
|
||||
<field name="inherit_id" ref="project.view_task_form2" />
|
||||
<field name="model">project.task</field>
|
||||
<field name="name">bemade_fsm.project_task.form2</field>
|
||||
<field name="arch" type="xml">
|
||||
<xpath
|
||||
expr="//field[@name='child_ids']/tree//field[@name='name']"
|
||||
position="after"
|
||||
>
|
||||
<field name="description" string="Description/Comments" />
|
||||
</xpath>
|
||||
<field name="user_ids" position="after">
|
||||
<field name="propagate_assignment" />
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
<!-- Add parent_id = false to domain for My Tasks, All Tasks: To Schedule, All Tasks and To Invoice-->
|
||||
<record id="industry_fsm.project_task_action_fsm" model="ir.actions.act_window">
|
||||
<field name="context">{'search_default_is_parent_task': True}</field>
|
||||
</record>
|
||||
<record id="project_task_view_list_fsm_inherit" model="ir.ui.view">
|
||||
<field name="name">project.task.view.list.fsm.inherit</field>
|
||||
<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">
|
||||
<attribute name="js_class">project_list</attribute>
|
||||
</tree>
|
||||
<field name="name" position="before">
|
||||
<field name="work_order_number" optional="show" />
|
||||
</field>
|
||||
<field name="company_id" position="attributes">
|
||||
<attribute name="optional">hide</attribute>
|
||||
</field>
|
||||
<field name="worksheet_template_id" position="attributes">
|
||||
<attribute name="optional">hide</attribute>
|
||||
</field>
|
||||
<field name="project_id" position="attributes">
|
||||
<attribute name="optional">hide</attribute>
|
||||
</field>
|
||||
<field name="progress" position="attributes">
|
||||
<attribute name="optional">hide</attribute>
|
||||
</field>
|
||||
<field name="activity_ids" position="attributes">
|
||||
<attribute name="optional">hide</attribute>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
<record id="industry_fsm.project_task_action_fsm_map" model="ir.actions.act_window">
|
||||
<field name="context">{'search_default_is_parent_task': True}</field>
|
||||
<field name="domain">[('is_fsm', '=', True),
|
||||
('project_id', '!=', False),
|
||||
('display_in_project', '=', True),
|
||||
('parent_id', '=', False)]
|
||||
</field>
|
||||
</record>
|
||||
<record
|
||||
id="industry_fsm.project_task_action_to_schedule_fsm"
|
||||
model="ir.actions.act_window"
|
||||
>
|
||||
<field name="context">{'search_default_is_parent_task': True}</field>
|
||||
<field name="domain">[('is_fsm', '=', True),
|
||||
('project_id', '!=', False),
|
||||
('display_in_project', '=', True),
|
||||
('parent_id', '=', False)]
|
||||
</field>
|
||||
</record>
|
||||
<record id="industry_fsm.project_task_action_all_fsm" model="ir.actions.act_window">
|
||||
<field name="context">{'search_default_is_parent_task': True}</field>
|
||||
<field name="domain">[('is_fsm', '=', True),
|
||||
('project_id', '!=', False),
|
||||
('display_in_project', '=', True),
|
||||
('parent_id', '=', False)]
|
||||
</field>
|
||||
</record>
|
||||
<record
|
||||
id="industry_fsm_sale.project_task_action_to_invoice_fsm"
|
||||
model="ir.actions.act_window"
|
||||
>
|
||||
<field name="context">{'search_default_is_parent_task': True}</field>
|
||||
<field name="domain">[('is_fsm', '=', True),
|
||||
('project_id', '!=', False),
|
||||
('display_in_project', '=', True),
|
||||
('parent_id', '=', False),
|
||||
('invoice_status', '=', 'to invoice')]
|
||||
</field>
|
||||
</record>
|
||||
<!-- Add parent_id = false to domain for planning actions as well -->
|
||||
<record
|
||||
id="industry_fsm.project_task_action_fsm_planning_groupby_user"
|
||||
model="ir.actions.act_window"
|
||||
>
|
||||
<field name="context">{'search_default_is_parent_task': True}</field>
|
||||
<field name="domain">[('is_fsm', '=', True),
|
||||
('project_id', '!=', False),
|
||||
('display_in_project', '=', True),
|
||||
('parent_id', '=', False)]
|
||||
</field>
|
||||
</record>
|
||||
<record
|
||||
id="industry_fsm.project_task_action_fsm_planning_groupby_project"
|
||||
model="ir.actions.act_window"
|
||||
>
|
||||
<field name="context">{'search_default_is_parent_task': True}</field>
|
||||
<field name="domain">[('is_fsm', '=', True),
|
||||
('project_id', '!=', False),
|
||||
('display_in_project', '=', True),
|
||||
('parent_id', '=', False)]
|
||||
</field>
|
||||
</record>
|
||||
<record
|
||||
id="industry_fsm_report.project_task_action_fsm_planning_groupby_worksheet"
|
||||
model="ir.actions.act_window"
|
||||
>
|
||||
<field name="context">{'search_default_is_parent_task': True}</field>
|
||||
<field name="domain">[('is_fsm', '=', True),
|
||||
('project_id', '!=', False),
|
||||
('display_in_project', '=', True),
|
||||
('parent_id', '=', False)]
|
||||
</field>
|
||||
</record>
|
||||
<!-- <record id="project_task_view_calendar_fsm" model="ir.ui.view">-->
|
||||
<!-- <field name="name">bemade_fsm.project_task_view_calendar_fsm</field>-->
|
||||
<!-- <field name="inherit_id" ref="industry_fsm.project_task_view_calendar_fsm"/>-->
|
||||
<!-- <field name="model">project.task</field>-->
|
||||
<!-- <field name="arch" type="xml">-->
|
||||
<!-- <field name="user_ids" position="attributes">-->
|
||||
<!-- <attribute name="filters">1</attribute>-->
|
||||
<!-- <attribute name="color">user_ids</attribute>-->
|
||||
<!-- </field>-->
|
||||
<!-- </field>-->
|
||||
<!-- </record>-->
|
||||
<record id="project_task_view_calendar_fsm_no_worksheet" model="ir.ui.view">
|
||||
<field name="name">bemade_fsm.project_task_view_calendar_no_worksheet</field>
|
||||
<field
|
||||
name="inherit_id"
|
||||
ref="industry_fsm_report.project_task_view_calendar_fsm_worksheet"
|
||||
/>
|
||||
<field name="model">project.task</field>
|
||||
<field name="priority" eval="100" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='worksheet_template_id']" position="replace" />
|
||||
<xpath expr="//calendar" position="attributes">
|
||||
<attribute name="color">user_ids</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<record id="project_task_view_search_fsm_inherit" model="ir.ui.view">
|
||||
<field name="name">project.task.view.search.fsm.inherit</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="inherit_id" ref="industry_fsm.project_task_view_search_fsm" />
|
||||
<field name="arch" type="xml">
|
||||
<filter name="schedule" position="attributes">
|
||||
<attribute name="domain">
|
||||
[
|
||||
'&',
|
||||
('fsm_done', '=', False),
|
||||
'|',
|
||||
('user_ids', '=', False),
|
||||
'&',
|
||||
('planned_date_start', '=', False),
|
||||
('date_deadline', '=', False),
|
||||
]
|
||||
</attribute>
|
||||
</filter>
|
||||
<filter name="my_tasks" position="after">
|
||||
<filter
|
||||
name="is_parent_task"
|
||||
string="Parent Task"
|
||||
domain="[('parent_id', '=', False)]"
|
||||
/>
|
||||
</filter>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
from . import new_task_from_template
|
||||
from . import res_config_settings
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
from odoo import models, fields
|
||||
|
||||
|
||||
class NewTaskFromTemplateWizard(models.TransientModel):
|
||||
_name = "project.task.from.template.wizard"
|
||||
_description = "Create Task from Template Wizard"
|
||||
|
||||
project_id = fields.Many2one(
|
||||
comodel_name="project.project",
|
||||
string="Project",
|
||||
help="The project the new task should be created in.",
|
||||
required=True,
|
||||
)
|
||||
|
||||
task_template_id = fields.Many2one(
|
||||
comodel_name="project.task.template",
|
||||
string="Task Template",
|
||||
help="The template to use when creating the new task.",
|
||||
required=True,
|
||||
)
|
||||
|
||||
new_task_title = fields.Char(
|
||||
help=(
|
||||
"The title (name) for the newly created task. If left blank, the name of"
|
||||
" the template will be used."
|
||||
),
|
||||
)
|
||||
|
||||
def default_get(self, fields_list):
|
||||
res = super().default_get(fields_list)
|
||||
active_id = self.env.context.get("active_id", False)
|
||||
active_model = self.env.context.get("active_model", False)
|
||||
if not active_model:
|
||||
params = self.env.context.get("params", False)
|
||||
active_model = params and params.get("model", False)
|
||||
if (
|
||||
active_model == "project.task.template"
|
||||
and active_id
|
||||
and "task_template_id" in fields_list
|
||||
):
|
||||
res.update({"task_template_id": active_id})
|
||||
if active_model == "project.task" and "project_id" in fields_list:
|
||||
res.update({"project_id": self.env.ref("industry_fsm.fsm_project").id})
|
||||
return res
|
||||
|
||||
def action_create_task_from_template(self):
|
||||
self.ensure_one()
|
||||
task = self.task_template_id.create_task_from_self(
|
||||
self.project_id, self.new_task_title
|
||||
)
|
||||
return {
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "project.task",
|
||||
"res_id": task.id,
|
||||
"view_mode": "form",
|
||||
"target": "current",
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="project_task_from_template_wizard_view_form" model="ir.ui.view">
|
||||
<field name="name">project.task.from.template.wizard.view.form</field>
|
||||
<field name="model">project.task.from.template.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="task_template_id"/>
|
||||
<field name="new_task_title"/>
|
||||
<field name="project_id"/>
|
||||
</group>
|
||||
</sheet>
|
||||
<footer>
|
||||
<button class='btn btn-primary' name="action_create_task_from_template" type="object" string="Create Task"/>
|
||||
<button class='btn btn-secondary' special="cancel" string="Cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record model="ir.actions.act_window" id="action_new_task_from_template">
|
||||
<field name="name">New Task from Template</field>
|
||||
<field name="res_model">project.task.from.template.wizard</field>
|
||||
<field name="binding_model_id" ref="bemade_fsm.model_project_task_template"/>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
from odoo import models, fields
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = "res.config.settings"
|
||||
|
||||
company_id = fields.Many2one(
|
||||
comodel_name="res.company",
|
||||
default=lambda self: self.env.company or self.env.user.company_id,
|
||||
)
|
||||
|
||||
separate_time_on_work_orders = fields.Boolean(
|
||||
string="Separate Time from Materials on Work Order",
|
||||
related="company_id.split_time_from_materials_on_service_work_orders",
|
||||
readonly=False,
|
||||
)
|
||||
|
||||
create_default_fsm_visit = fields.Boolean(
|
||||
string="Create Default Visit for FSM Sales Orders",
|
||||
related="company_id.create_default_fsm_visit",
|
||||
readonly=False,
|
||||
)
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="res_config_settings_view_form" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form.inherit.bemade.fsm</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="industry_fsm.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<setting name="module_industry_fsm_report" position="after">
|
||||
<setting
|
||||
name="separate_time_on_work_orders"
|
||||
help="Separate blocks for materials and time on work order reports."
|
||||
company_dependent="1">
|
||||
<field name="separate_time_on_work_orders"/>
|
||||
</setting>
|
||||
<setting
|
||||
name="create_default_fsm_visit"
|
||||
help="Create a default FSM visit if a service SO doesn't already contain one at confirmation."
|
||||
company_dependent="1">
|
||||
<field name="create_default_fsm_visit"/>
|
||||
</setting>
|
||||
</setting>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -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
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue