job sheet workflow

This commit is contained in:
Florian du Garage Num 2024-04-07 14:14:33 +02:00
parent 69b6db9dea
commit df15154f24
14 changed files with 993 additions and 65 deletions

View File

@ -15,7 +15,7 @@ Addons for Odoo 16.
| gn_contract_amendment | 16.0.0.0.1 | Amendments to Hr Contract with Prototype Inheritance |
| gn_contract_validation | 16.0.0.0.1 | Simple class inheritance from HrContract to add some states |
| gn_job_sheet | 16.0.0.0.4 | Fiche de poste |
| gn_contract_situation | 16.0.0.0.1 | Multiple Delegation Inheritance to associate original contract, amendments, job sheets |
| gn_contract_situation | 16.0.0.0.3 | Multiple Delegation Inheritance to associate original contract, amendments, job sheets |
## ToDo
@ -38,4 +38,4 @@ Addons for Odoo 16.
- [] compute contract.cc_id from contract.company_id.cc (cf issue #4)
- [x] debug entretien start date on creation (cf issue #5)
- [x] debug numérotation des entretiens par salarié on creation (cf issue #6)
- [] better handling of event creation from entretien (cf issue #7)
- [] better handling of event creation from entretien (cf issue #7)

View File

@ -14,7 +14,15 @@ It uses delegation inheritance, which embed the inherited objects in the current
# Changelog
- v16.0.0.0.3 (20204/04/07)
- Job Sheet Creation Workflow with wizards
- v16.0.0.0.2 (2024/03/24)
- First form view
- v16.0.0.0.1 (2024/03/23)
- inital creation
# Issues
- [] Need to delete situation records when writtent without sheet_id nor amendment_id
- [] Freeze active records until expiration
- [] Debug auto situation creation on amendments and sheets creation

View File

@ -1,6 +1,6 @@
{
"name": "Gestion des Contrats: Situation de l'éxécution du contrat",
"version": "16.0.0.0.2",
"version": "16.0.0.0.3",
"category": "HR",
"summary": "Regroupe les éléments d'évolution du contrat et de son exécution",
"author": "Le Garage Numérique",
@ -8,13 +8,17 @@
"website": "https://odoo.legaragenumerique.fr",
"depends": [
"hr",
"hr_contract",
"gn_contract_amendment",
"gn_job_sheet",
"hr_employee_calendar_planning", # from https://github.com/OCA/hr
],
"data": [
'views/gn_job_sheet.xml',
'views/gn_contract_amendment.xml',
'views/gn_contract_history.xml',
'views/gn_contract_situation.xml',
'data/gn_contract_situation.xml',
#'data/gn_contract_situation_cron.xml',
"security/ir.model.access.csv",
],
"license": "LGPL-3",

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<menuitem
id="gn_contract_situation.situations_menu"
name="Conditions d'éxecution"
parent="hr.menu_hr_employee_payroll"
sequence="400"
action="action_contract_situation"
groups="hr.group_hr_manager"/>
</odoo>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<data noupdate="1">
<record id="cron_update_situation_childs_status" model="ir.cron">
<field name="name">Update HR Status</field>
<field name="model_id" ref="model_hr_contract_situation"/>
<field name="state">code</field>
<field name="code">model.cron_update_situation_childs_status()</field>
<field name="active" eval="True"/>
<field name="user_id" ref="base.user_root"/>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field name="nextcall" eval="(datetime.now() + timedelta(hours=1)).strftime('%Y-%m-%d 00:10:00')"/>
<field name="doall" eval="False"/>
</record>
</data>
</odoo>

View File

@ -2,4 +2,6 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import gn_contract
from . import gn_contract_situation
from . import gn_contract_situation
from . import gn_job_sheet
from . import gn_amendment

View File

@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, api, _
from odoo.exceptions import ValidationError
from datetime import timedelta
import logging
_logger = logging.getLogger(__name__)
class GnHrContractAmendment(models.Model):
_inherit = 'hr.contract.amendment'
situation_ids = fields.One2many('hr.contract.situation', inverse_name='amendment_id', string="Conditions d'éxecution", readonly=True)
class AmendmentConfirmationWizard(models.TransientModel):
_name = 'amendment.confirmation.wizard'
_description = 'Confirm Amendment Creation'
message = fields.Text(default="Are you sure you want to create a new amendment?")
def confirm_amendment_creation(self):
active_id = self.env.context.get('active_id')
# active_id = self.env.context.get('active_id')
# situation = self.env['hr.contract.situation'].browse(active_id)
# action = self.env.context.get('action')
# if situation:
# new_situation = situation.copy(default={'amendment_id': False})
# action['context'].update({
# 'default_situation_ids': [(4, new_situation.id)],
# })
# return action
# else:
# return
action = self.env.context.get('action')
# action['context'].update({
# 'default_previous_amendment_id': active_id
# })
return action
def continue_pending_amendment(self):
pending_id = self.env.context.get('pending_id')
action = self.env.context.get('action')
action['res_id'] = pending_id
return action

View File

@ -9,10 +9,11 @@ import logging
_logger = logging.getLogger(__name__)
class GnHrContract(models.Model):
_inherit = "hr.contract"
_inherit = 'hr.contract'
situation_ids = fields.One2many('hr.contract.situation', inverse_name='contract_id', string="Conditions d'éxecution", readonly=True)
actual_situation_id = fields.Many2one('hr.contract.situation', compute='_compute_actual_situation_id', string="Conditions d'éxecution en vigueur")
active_situation_id = fields.Many2one('hr.contract.situation', compute='_compute_active_situation_id', string="Conditions d'éxecution en vigueur")
pending_situation_id = fields.Many2one('hr.contract.situation', compute='_compute_pending_situation_id', string="Conditions d'éxecution en préparation")
state = fields.Selection([
('draft', 'Brouillon'),
@ -21,29 +22,58 @@ class GnHrContract(models.Model):
('wait_employee_approval', "En attente de signature par le salarié"),
('ready', 'Prête'),
('active', 'Active'),
('expired', 'Expiré')
], string="État", default='draft', required=True)
@api.depends('situation_ids.state')
def _compute_actual_situation_id(self):
@api.depends('situation_ids', 'situation_ids.state')
def _compute_active_situation_id(self):
for contract in self:
active_situation = contract.situation_ids.filtered(lambda s: s.state == 'active')
if len(active_situation) > 1:
raise ValidationError("Il ne peut y avoir qu'une condition d'exécution active à la fois.")
if len(active_situation) == 0:
contract.actual_situation_id = False
elif len(active_situation) == 1:
contract.active_situation_id = active_situation[:1]
else:
contract.actual_situation_id = active_situation[:1]
contract.active_situation_id = False
@api.depends('situation_ids', 'situation_ids.state')
def _compute_pending_situation_id(self):
for contract in self:
pending_situation = contract.situation_ids.filtered(lambda s: s.state != 'active' and s.state != 'expired')
if len(pending_situation) > 1:
contract.pending_situation_id = pending_situation[:1]
#raise ValidationError("Il ne peut y avoir qu'une condition d'exécution en préparation à la fois.")
elif len(pending_situation) == 1:
contract.pending_situation_id = pending_situation[:1]
else:
contract.pending_situation_id = False
# def generate_situations():
# @api.depends('situation_ids')
# def _compute_situations(self):
# for contract in self:
# situations = self.env['hr.contract.situation'].search(['contract_id', '=', contract.id])
# sheets = self.env['hr.job.sheet'].search(['contract_id', '='])
def action_open_contract_situation(self):
self.ensure_one()
action = self.env['ir.actions.actions']._for_xml_id('gn_contract_situation.action_contract_situation')
if self.actual_situation_id:
active_situation = self.active_situation_id
pending_situation = self.pending_situation_id
if active_situation:
action.update({
'view_mode': 'form',
'view_id': self.env.ref('gn_contract_situation.contract_situation_view_form').id,
'views': [(self.env.ref('gn_contract_situation.contract_situation_view_form').id, 'form')],
'res_id': self.actual_situation_id.id,
'res_id': active_situation.id,
})
elif pending_situation:
action.update({
'view_mode': 'tree',
})
else:
default_vals = {
@ -56,4 +86,4 @@ class GnHrContract(models.Model):
'views': [(self.env.ref('gn_contract_situation.contract_situation_view_form').id, 'form')],
'res_id': new_situation.id,
})
return action
return action

View File

@ -19,14 +19,20 @@ class GnHrContractSituation(models.Model):
# 'hr.job.sheet': 'sheet_id'
# }
#Those ondelete=cascade should become rectrict once everything is solid
contract_id = fields.Many2one('hr.contract', required=True, auto_join=True, ondelete="cascade")
amendment_id = fields.Many2one('hr.contract.amendment', ondelete="cascade")
sheet_id = fields.Many2one('hr.job.sheet', ondelete="cascade")
#calendar_id = fields.Many2one('hr.employee.calendar', ondelete="cascade")
name = fields.Char(string="Nom", related='contract_id.name')
employee_id = fields.Many2one('hr.employee', related='contract_id.employee_id')
date_from = fields.Date(string="Fin de validité", compute='_compute_date_from')
date_to = fields.Date(string="Fin de validité", compute='_compute_date_to')
sheet_name = fields.Char(string="Nom de la fiche de poste", related='sheet_id.name')
amendment_name = fields.Char(string="Nom de l'avenant'", related='amendment_id.name')
date_from = fields.Date(string="Début de validité")#, compute='_compute_date_from')
date_to = fields.Date(string="Fin de validité")#, compute='_compute_date_to')
state = fields.Selection([
('draft', 'Brouillon'),
@ -35,34 +41,384 @@ class GnHrContractSituation(models.Model):
('wait_employee_approval', "En attente de signature par le salarié"),
('ready', 'Prête'),
('active', 'Active'),
], string="État", default='draft', compute='_compute_state', required=True)
('expired', "Expirée"),
], string="État", default='draft')#, compute='_compute_state', required=True)
@api.depends('contract_id.state', 'amendment_id.state', 'sheet_id.state')
def _compute_state(self):
for record in self:
if record.contract_id and record.contract_id.state == 'active' and \
record.amendment_id and record.amendment_id.state == 'active' and \
record.sheet_id and record.sheet_id.state == 'active':
record.state = 'active'
else:
record.state = 'draft'
# @api.depends('contract_id.state', 'amendment_id.state', 'sheet_id.state')
# def _compute_state(self):
# for situation in self:
# objects = [situation.contract_id, situation.amendment_id, situation.sheet_id]
# defined_states = [object.state for object in objects if object and object.state]
# if all(state == 'active' for state in defined_states):
# situation.state = 'active'
@api.depends('contract_id.date_start', 'amendment_id.date_start', 'sheet_id.date_start')
def _compute_date_from(self):
for situation in self:
starts = [situation.contract_id.date_start, situation.amendment_id.date_start, situation.sheet_id.date_start]
valid_starts = [start for start in starts if start]
if valid_starts:
situation.date_from = max(valid_starts)
else:
situation.date_from = None
# @api.depends('contract_id.date_start', 'amendment_id.date_start', 'sheet_id.date_start')
# def _compute_date_from(self):
# for situation in self:
# starts = [situation.contract_id.date_start, situation.amendment_id.date_start, situation.sheet_id.date_start, situation.calendar_id.date_start]
# valid_starts = [start for start in starts if start]
# if valid_starts:
# situation.date_from = max(valid_starts)
# else:
# situation.date_from = None
@api.depends('contract_id.date_end', 'amendment_id.date_end', 'sheet_id.date_end')
def _compute_date_to(self):
for situation in self:
ends = [situation.contract_id.date_end, situation.amendment_id.date_end, situation.sheet_id.date_end]
valid_ends = [end for end in ends if end]
if valid_ends:
situation.date_to = min(valid_ends)
# @api.depends('contract_id.date_end', 'amendment_id.date_end', 'sheet_id.date_end')
# def _compute_date_to(self):
# for situation in self:
# ends = [situation.contract_id.date_end, situation.amendment_id.date_end, situation.sheet_id.date_end, situation.calendar_id.date_start]
# valid_ends = [end for end in ends if end]
# if valid_ends:
# situation.date_to = min(valid_ends)
# else:
# situation.date_to = None
#Cron des status à repenser
# def cron_update_situation_childs_status(self):
# today = fields.Date.context_today(self)
# for records in [
# self.env['hr.contract'].search([('state', '=', 'active'), ('date_end', '<', today)]),
# self.env['hr.contract.amendment'].search([('state', '=', 'active'), ('date_end', '<', today)]),
# self.env['hr.job.sheet'].search([('state', '=', 'active'), ('date_end', '<', today)])
# ]:
# for record in records:
# records.state = 'expired'
# for records in [
# self.env['hr.contract'].search([('state', '=', 'ready'), ('date_start', '<=', today)]),
# self.env['hr.contract.amendment'].search([('state', '=', 'ready'), ('date_start' , '<=', today)]),
# self.env['hr.job.sheet'].search([('state', '=', 'ready'), ('date_start', '<=', today)]),
# ]:
# for record in records:
# records.state = 'active'
## Ici il faut créer 2 activités:
# - une première activité pour signaler quand un contrat, un avenant ou une feuille de poste commencent dans 2 jours mais
# n'est toujours pas à l'état ready
# - une activité pour signaler qu'un des éléments est passé en status 'active', pour que les fiches de paye puissent être établis, mais qu'il attend encore telle ou telle étape.
# dans ce cas, il faut modifier le code cron pour passer active les éléments qui ne sont pas en expired ou draft.
def define_first_sheet(self):
_logger.warning("Enter in define_first_sheet with situation id %s", self.id)
_logger.warning("Associated contract is %s - %s", self.contract_id.id, self.contract_id.name)
action = {
'type': 'ir.actions.act_window',
'name': 'Modification de la fiche de poste',
'view_mode': 'form',
'res_model': 'hr.job.sheet',
'target': 'current',
'context': {
'default_situation_ids': [(4, self.id)],
'default_contract_id': self.contract_id.id,
'default_date_start': self.contract_id.date_start,
'default_date_end': self.contract_id.date_end,
'default_previous_sheet_id': False,
}
}
return action
def ask_create_or_modify(self, type, active_id, pending_id):
_logger.warning("enter in ask_create_or_modify with situation id %s, type = %s, active_id = %s, pending_id = %s", self.id, type, active_id, pending_id)
self.ensure_one()
action = {
'type': 'ir.actions.act_window',
'view_mode': 'form',
'target': 'current',
'context': {
#'default_situation_ids': [(4, self.id)],
'default_contract_id': self.contract_id.id,
#'default_date_start': new_date_start,
#'default_date_end'contract': self.contract_id.id': self.contract_id.date_end,
#'default_previous_sheet_id': active_id if active_id else None,
}
}
wizard = {
'type': 'ir.actions.act_window',
'name': 'Confirmation',
'view_mode': 'form',
'target': 'new',
'context': {
#'active_id': active_id,
"message": "test",
'action': action,
'active_id': active_id if active_id else None,
'pending_id': pending_id if pending_id else None
},
}
if type == 'sheet':
action['res_model'] = 'hr.job.sheet'
action['name'] = "Modification de la fiche de poste"
wizard['res_model'] = 'sheet.confirmation.wizard'
active_sheet = self.env['hr.job.sheet'].browse(active_id) if active_id else False
pending_sheet = self.env['hr.job.sheet'].browse(pending_id) if pending_id else False
if active_sheet:
wizard['context']['message'] = (_("Il existe une fiche de poste active: %s \n\n", active_sheet.name))
if pending_sheet:
wizard['context']['message'] += (_("Il existe aussi une fiche de poste en préparation: %s \n\n", pending_sheet.name))
elif pending_sheet:
wizard['context']['message'] = (_("Il existe une fiche de poste en cours de préparation: %s \n\n", pending_sheet.name))
wizard['context']['message'] += (_("Que voulez-vous faire ?"))
#wizard['context']['active_id'] = None # self.contract_id.active_sheet_id.id
elif type == 'amendment':
model = 'hr.contract.amendment'
wizard_model = 'amendment.confirmation.wizard'
action_name = "Modification de l'avenant"
active_amendment = self.env['hr.contract.amendment'].browse(active_id) if active_id else None
pending_amendment = self.env['hr.contract.amendment'].browse(pending_id) if pending_id else None
if active_amendment:
message = (_("Il existe un \navenant actif - %s - , que voulez-vous faire?&#10", active_amendment.name))
if pending_amendment:
message.append(_("Il existe aussi un avenant en préparation - %s", pending_amendment.name))
elif pending_amendment:
message = (_("Il existe un avenant en cours de préparation - %s", pending_amendment.name))
active_id = self.amendment_id.id
elif type == 'calendar':
model = 'resource.calendar'
wizard_model = 'calendar.confirmation.wizard'
action_name = "Modification du planning"
message = "Voulez-vous modifier l'emploi du temps actif?"
active_id = self.calendar_id.id if self.calendar_id else None
_logger.warning("Associated contract when calling sheet form from situation form: %s", self.contract_id)
#return wizard
wizard_create = self.env[wizard['res_model']].create({})
wizard['res_id'] = wizard_create.id
_logger.warning("Wizard generated: %s", wizard)
return wizard
# def ask_for_temporary(self, type):
# #Here we need to create a wizard and get True or False
# if type == 'sheet':
# res_model = 'sheet.confirmation.wizard'
# message = "Voulez-vous modifier la fiche de poste active?"
# elif type == 'amendment':
# res_model = 'amendment.confirmation.wizard'
# message = "Voulez-vous modifier l'avenant actif?"
# elif type == 'calendar':
# res_model = 'calendar.confirmation.wizard'
# message = "Voulez-vous modifier l'emploi du temps actif?"
# wizard = {
# 'type': 'ir.actions.act_window',
# 'name': 'Confirmation',
# 'res_model': res_model,
# 'view_mode': 'form',
# 'target': 'new',
# 'context': {
# 'default_message': message,
# 'active_id': self.id,
# 'action': action,
# },
# }
# if type == 'sheet':
# # Tell Wizard its a sheet
# return True
# elif type == 'amendment':
# # Tell wizard its amendment
# return True
# elif type == 'calendar':
# # Tell wizard its calendar
def action_define_sheet(self):
_logger.warning("Enter in action define sheet in situation %s", self.id)
self.ensure_one()
_logger.warning("Actual sheet_id: %s", self.sheet_id.id)
_logger.warning("Actual contract_id: %s", self.contract_id.id)
pending_sheet_id = self.contract_id.pending_sheet_id.id if self.contract_id.pending_sheet_id else False
if not self.sheet_id and not pending_sheet_id:
action = self.define_first_sheet()
return action
else:
active_sheet_id = self.contract_id.active_sheet_id.id if self.contract_id.active_sheet_id else False
# Call a wizard to ask if:
# - they want to modify the sheet in preparation (if it exists)
# - or create a temporary sheet
# - modify the existing sheet non-permanently (if there is none in preparation)
_logger.warning("going to ask or create with active id: %s and pending id: %s", active_sheet_id, pending_sheet_id)
wizard = self.ask_create_or_modify(type='sheet', active_id=active_sheet_id, pending_id=pending_sheet_id)
return wizard
#
# Ask if its temporary or permanent
# is_temporary = self.ask_for_temporary(type='sheet')
# if is_temporary:
# action = {
# 'type': 'ir.actions.act_window',
# 'name': 'Modification de la fiche de poste',
# 'view_mode': 'form',
# 'res_model': 'hr.job.sheet',
# 'target': 'current',
# 'context': {
# 'default_situation_ids': [(4, self.id)],
# 'default_contract_id': self.contract_id.id,
# 'default_date_start': new_date_start,
# 'default_date_end': self.contract_id.date_end,
# 'default_previous_sheet_id': self.sheet_id.id,
# }
# }
# _logger.warning("Actual situation has sheet_id : %s", self.sheet_id.id)
# if not self.sheet_id or (self.sheet_id and self.sheet_id.state == 'active'):
# tomorrow = fields.Date.context_today(self) + timedelta(days=1)
# if not self.sheet_id:
# new_date_start = self.contract_id.date_start
# elif self.sheet_id and self.sheet_id.is_default:
# contract_start = self.contract_id.sheet_id.date_start
# contract_end = self.contract_id.date_end
# if tomorrow >= contract_start and tomorrow <= contract_end:
# new_date_start = tomorrow
# elif tomorrow < contract_start:
# new_date_start = contract_start
# else:
# new_date_start = contract_end
# new_date_start = tomorrow if tomorrow >= self.contract_id.sheet_id.date_start and tomorrow <= self.contract_id.date_end \
# else self.contract_id.date_start if tomorrow < self.contract_id.sheet_id.date_start \
# else self.contract_id.date_end
# elif self.sheet_id.date_end and self.contract_id.date_end and self.sheet_id.date_end < self.contract_id.date_end:
# new_date_start = self.sheet_id.date_end + timedelta(days=1)
# else:
# if self.contract_id.date_end and tomorrow > self.contract_id.date_end:
# tomorrow = self.contract_id.date_end
# new_date_start = tomorrow
#Ici, il faut traiter la gestion des situations:
# Sile contrat a une fiche de poste valide, et qu'on crée un avenant,
# il ne faut pas que la situation se décale il faut:
# - réduire la date de fin de la 1ere situation avec
# * sit1.date_from = sheet.date_start et sit1.date_to = amendment.date_start - 1
# créer une 2e situation avec
# * sit2.date_from = amendment.date_start et sit2.date_to = amendment.date_end
# créer une 3e situation si amendment.date_end < sheet.date_end:
# * sit3.date_from = amendment.date_end + 1 et sit3.date_to = sheet.date_end
# action.update({
# 'context': {
# 'default_situation_ids': [(4, self.id)],
# 'default_contract_id': self.contract_id.id,
# 'default_date_start': new_date_start,
# 'default_date_end': self.contract_id.date_end,
# 'default_previous_sheet_id': self.sheet_id.id,
# }
# })
# if self.sheet_id and self.sheet_id.state == 'active':
# related_situations = self.contract_id.situation_ids.filtered(lambda s: s.sheet_id and s.sheet_id.state not in ['active', 'expired'])
# pending_sheet_id = {situation.sheet_id.id for situation in related_situations}
# _logger.warning("Pending sheet id : %s", list(pending_sheet_id)[0] if pending_sheet_id else None )
# if len(pending_sheet_id) > 1:
# raise ValidationError("More thant 1 sheet in preparation !")
# elif pending_sheet_id:
# action['res_id'] = list(pending_sheet_id)[0]
# return {
# 'type': 'ir.actions.act_window',
# 'name': 'Confirmation',
# 'res_model': 'sheet.confirmation.wizard',
# 'view_mode': 'form',
# 'target': 'new',
# 'context': {
# 'default_message': 'Voulez-vous modifier la fiche de poste active?',
# 'active_id': self.id,
# 'action': action,
# },
# }
# else:
# action['res_id'] = self.sheet_id.id
# if self.sheet_id.state == 'expired':
# action.update({
# 'flags': {'mode':'readonly'}
# })
# return action
def action_define_amendment(self):
self.ensure_one()
action = {
'type': 'ir.actions.act_window',
'name': "Création d'un avenant",
'view_mode': 'form',
'res_model': 'hr.contract.amendment',
'target': 'current',
'view_id': self.env.ref('gn_contract_situation.gn_contract_amendment_form_with_situation').id,
}
_logger.warning("Actual situation has amendment_id : %s", self.amendment_id.id)
# Si la situation n'est pas encore associée à un avenant ou si elle est associée à un avenant actif:
if not self.amendment_id or (self.amendment_id and self.amendment_id.state == 'active'):
tomorrow = fields.Date.context_today(self) + timedelta(days=1)
# Si il n'y a pas d'avenant, ou que l'avenant n'a pas de date de fin, on met la date de début par défaut au lendemain de l'exécution de l'action ou au début du contrat.
if not self.amendment_id or not self.amendment_id.date_end:
new_date_start = tomorrow if tomorrow < self.contract_id.date_start and ((not self.contract_id.date_end) or (tomorrow <= self.contract_id.date_end)) else self.contract_id.date_start
# Si la situation a un avenant actif avec une date de fin définie avant la fin du contrat,
# on met la date de début au lendemain de la fin de l'avenant actif
elif self.amendment_id.date_end and (
(self.contract_id.date_end and self.amendment_id.date_end < self.contract_id.date_end)
or not self.contract_id.date_end):
new_date_start = self.amendment_id.date_end + timedelta(days=1)
# Le dernier cas est celui où l'avenant termine en même temps que contrat, on raise une Error pour le moment,
# en attendant de bien concevoir l'impact sur les dates de contrat de la création d'avenant.
else:
situation.date_to = None
raise ValidationError("Impossible de créer un avenant quand l'avenant précédent se termine en même temps que son contrat associé")
# if self.contract_id.date_end and tomorrow > self.contract_id.date_end:
# tomorrow = self.contract_id.date_end
# new_date_start = tomorrow
action.update({
'context': {
'default_situation_ids': [(4, self.id)],
'default_contract_id': self.contract_id.id,
'default_date_start': new_date_start,
'default_date_end': self.contract_id.date_end,
'default_previous_amendment_id': self.amendment_id.id,
}
})
if self.amendment_id and self.amendment_id.state == 'active':
related_situations = self.contract_id.situation_ids.filtered(lambda s: s.amendment_id and s.amendment_id.state not in ['active', 'expired'])
pending_amendment_id = {situation.amendment_id.id for situation in related_situations}
_logger.warning("Pending amendment id : %s", list(pending_amendment_id)[0] if pending_amendment_id else None)
if len(pending_amendment_id) > 1:
raise ValidationError("More thant 1 amendment in preparation !")
elif pending_amendment_id:
action['res_id'] = list(pending_amendment_id)[0]
return {
'type': 'ir.actions.act_window',
'name': 'Confirmation',
'res_model': 'amendment.confirmation.wizard',
'view_mode': 'form',
'target': 'new',
'context': {
'default_message': "Voulez-vous modifier l'avenant actif?",
'active_id': self.id,
'action': action,
},
}
else:
action['res_id'] = self.amendment_id.id
if self.sheet_id.state == 'expired':
action.update({
'flags': {'mode':'readonly'}
})
return action

View File

@ -0,0 +1,290 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, api, _
from odoo.exceptions import ValidationError
from datetime import timedelta
import logging
_logger = logging.getLogger(__name__)
class GnHrJobSheet(models.Model):
_inherit = 'hr.job.sheet'
situation_ids = fields.One2many('hr.contract.situation', inverse_name='sheet_id', string="Conditions d'éxecution", readonly=True)
previous_sheet_id = fields.Many2one('hr.job.sheet', string="Fiche de poste précédente", readonly=True)
# sheet.pending_situation_id = False
# active_situation_id = fields.Many2one('hr.contract.situation', compute='_compute_active_situation_id', string="Conditions d'éxecution en vigueur")
# pending_situation_id = fields.Many2one('hr.contract.situation', compute='_compute_pending_situation_id', string="Conditions d'éxecution en préparation")
# @api.depends('situation_ids', 'situation_ids.state')
# def _compute_active_situation_id(self):
# for sheet in self:
# active_situation = sheet.situation_ids.filtered(lambda s: s.state == 'active')
# if len(active_situation) > 1:
# raise ValidationError("Il ne peut y avoir qu'une condition d'exécution active à la fois.")
# elif len(active_situation) == 1:
# sheet.active_situation_id = active_situation[:1]
# else:
# sheet.active_situation_id = False
# @api.depends('situation_ids', 'situation_ids.state')
# def _compute_pending_situation_id(self):
# for sheet in self:
# pending_situation = sheet.situation_ids.filtered(lambda s: s.state != 'active' and s.state != 'expired')
# if len(pending_situation) > 1:
# sheet.pending_situation_id = pending_situation[:1]
# #raise ValidationError("Il ne peut y avoir qu'une condition d'exécution en préparation à la fois.")
# elif len(pending_situation) == 1:
# sheet.pending_situation_id = pending_situation[:1]
# else:
# sheet.pending_situation_id = False
# I think this is not necessary anymore
# ## Checks for start_date
# def start_date_before_previous_sheet(self):
# if self.previous_sheet_id and self.date_start and self.date_start < self.previous_sheet_id.date_start:
# raise ValidationError("The start date cannot be before the previous sheet start date.")
# def start_date_equals_previous_sheet(self):
# if self.previous_sheet_id and self.date_start and self.date_start == self.previous_sheet_id.date_start:
# raise ValidationError("The start date cannot be the same day than the previous sheet start date.")
# @api.constrains('date_start')
# def _limit_date_start(self):
# for sheet in self:
# sheet.start_date_before_previous_sheet()
# sheet.start_date_equals_previous_sheet()
# if self.previous_sheet_id:
# self.previous_sheet_id.date_end = self.date_start - timedelta(days=1)
# @api.onchange('date_start')
# def _onchange_date_start(self):
# self.start_date_before_previous_sheet()
# self.start_date_equals_previous_sheet()
# if self.previous_sheet_id:
# self.previous_sheet_id.date_end = self.date_start - timedelta(days=1)
# ## Checks for end_date
# def end_date_before_previous_sheet(self):
# if self.previous_sheet_id and self.previous_sheet_id.date_end and self.date_end and self.date_end < self.previous_sheet_id.date_end:
# raise ValidationError("The end date cannot be before the previous sheet end date.")
# def end_date_equals_previous_sheet(self):
# if self.previous_sheet_id and self.previous_sheet_id.date_end and self.date_end and self.date_end == self.previous_sheet_id.date_end:
# # dans le cas où le jour de début la prise de poste est le même que celle qui est déjà active,
# # on doit insérer ici une confirmation,
# #pour modifier la fiche de poste précédente, et supprimer celle en cours
# # ainsi que la nouvelle situation associée
# _logger.warning("Wizard à insérer ici pour écraser la fiche de poste active")
# raise ValidationError("The end date cannot be the same day than the previous sheet end date.")
# @api.constrains('date_end')
# def _limit_date_end(self):
# for sheet in self:
# sheet.end_date_before_previous_sheet()
# sheet.end_date_equals_previous_sheet()
# @api.onchange('date_end')
# def _onchange_date_end(self):
# self.end_date_before_previous_sheet()
# self.end_date_equals_previous_sheet()
# # if self.previous_sheet_id:
# # self.previous_sheet_id.date_end = self.date_end - timedelta(days=1)
class SheetDaterangeWizard(models.TransientModel):
_name = 'sheet.daterange.wizard'
_description = 'Select dates'
message = fields.Text(compute='_compute_message')
is_permanent = fields.Boolean(string="Remplacement permanent", default=True, compute='_compute_permanency')
date_start = fields.Date(string="Date de début", default=lambda self: fields.Date.context_today(self))
date_end = fields.Date(string="Date de fin", default=lambda self: self._compute_default_date_end())
def _compute_default_date_end(self):
active_id = self._context.get('active_id')
if active_id:
previous_sheet = self.env['hr.job.sheet'].browse(active_id)
return previous_sheet.date_end
return False
def _compute_message(self):
_logger.warning("Message: %s", self._context.get('message'))
for record in self:
#_logger.warning("Message: %s", self._context.get('message'))
record.message = self._context.get('message')
def _compute_permanency(self):
for record in self:
record.is_permanent = self._context.get('is_permanent')
def confirm_dates(self):
active_id = self._context.get('active_id')
#previous_sheet = self.env['hr.job.sheet'].browse(active_id)
action = self._context.get('action')
action['context'].update({
'default_previous_sheet_id': active_id,
'default_date_start': self.date_start,
'default_date_end': self.date_end,
})
return action
class SheetTemporalityWizard(models.TransientModel):
_name = 'sheet.temporality.wizard'
_description = 'Ask if temporary or permanent'
message = fields.Text(compute='_compute_message')
def _compute_message(self):
_logger.warning("Message: %s", self._context.get('message'))
for record in self:
#_logger.warning("Message: %s", self._context.get('message'))
record.message = self._context.get('message')
def is_temporary(self):
active_id = self._context.get('active_id')
previous_sheet = self.env['hr.job.sheet'].browse(active_id)
action = self._context.get('action')
action['context'].update({
'default_previous_sheet_id': active_id,
'default_date_start': fields.Date.context_today(self),
'default_date_end': previous_sheet.date_end
})
wizard = {
'type': 'ir.actions.act_window',
'name': 'Confirmation',
'view_mode': 'form',
'target': 'new',
'res_model': 'sheet.daterange.wizard',
'context': {
"is_permanent": False,
"message": (_("Vous avez choisi une modification temporaire de la fiche de poste active.\nSur quelle période voulez-vous que s'applique le changement?")),
'action': action,
'active_id': active_id if active_id else None,
},
}
wizard_create = self.env['sheet.daterange.wizard'].create({})
wizard['res_id'] = wizard_create.id
_logger.warning("Wizard generated: %s", wizard)
return wizard
def is_permanent(self):
active_id = self._context.get('active_id')
previous_sheet = self.env['hr.job.sheet'].browse(active_id)
action = self._context.get('action')
action['context'].update({
'default_previous_sheet_id': active_id,
'default_date_end': previous_sheet.date_end
})
wizard = {
'type': 'ir.actions.act_window',
'name': 'Confirmation',
'view_mode': 'form',
'target': 'new',
'res_model': 'sheet.daterange.wizard',
'context': {
"is_permanent": True,
"message": (_("Vous avez choisi une modification permanente de la fiche de poste active.\nÀ partir de quelle date voulez-vous que s'applique le changement?")),
'action': action,
'active_id': active_id if active_id else None,
},
}
wizard_create = self.env['sheet.daterange.wizard'].create({})
wizard['res_id'] = wizard_create.id
_logger.warning("Wizard generated: %s", wizard)
return wizard
class SheetConfirmationWizard(models.TransientModel):
_name = 'sheet.confirmation.wizard'
_description = 'Confirm Sheet Creation'
message = fields.Text(compute='_compute_message', precompute=True)
has_active_id = fields.Boolean(compute='_compute_has_active_id', precompute=True)
has_pending_id = fields.Boolean(compute='_compute_has_pending_id', precompute=True)
def _compute_message(self):
_logger.warning("Message: %s", self._context.get('message'))
for record in self:
#_logger.warning("Message: %s", self._context.get('message'))
record.message = self._context.get('message')
def _compute_has_active_id(self):
for record in self:
active_id = self._context['active_id']
record.has_active_id = True if active_id and active_id != None else False
_logger.warning("has_active_id: %s", record.has_active_id)
def _compute_has_pending_id(self):
for record in self:
pending_id = self._context['pending_id']
record.has_pending_id = True if pending_id and pending_id != None else False
_logger.warning("has_pending_id: %s", record.has_pending_id)
def confirm_sheet_creation(self):
active_id = self._context.get('active_id')
previous_sheet = self.env['hr.job.sheet'].browse(active_id)
# situation = self.env['hr.job.sheet'].browse(active_id)
# action = self.env.context.get('action')
# contract = self.env.context.get('contract_id')
# if situation:
# #new_situation = situation.copy(default={'sheet_id': False})
# contract_id = contract
# action['context'].update({
# 'default_situation_ids': [(4, new_situation.id)],
# })
# return action
# else:
# return
action = self._context.get('action')
action['context'].update({
'default_previous_sheet_id': active_id,
})
wizard = {
'type': 'ir.actions.act_window',
'name': 'Confirmation',
'view_mode': 'form',
'target': 'new',
'res_model': 'sheet.temporality.wizard',
'context': {
"message": (_("La fiche de poste actuelle est: %s ,\n\nVoulez-vous créer une fiche de poste temporaire pour faire état d'un changement ponctuel de missions,\n\nou voulez-vous modifier la fiche de poste permanente du salarié?\n\n", previous_sheet.name)),
'action': action,
'active_id': active_id if active_id else None,
},
}
wizard_create = self.env['sheet.temporality.wizard'].create({})
wizard['res_id'] = wizard_create.id
_logger.warning("Wizard generated: %s", wizard)
return wizard
def see_active_sheet(self):
active_id = self._context.get('active_id')
action = self._context.get('action')
action['res_id'] = active_id
action['flags'] = {'mode': 'readonly'}
#_logger.warning("action: %s", action)
return action
def continue_pending_sheet(self):
pending_id = self._context.get('pending_id')
action = self._context.get('action')
action['res_id'] = pending_id
#_logger.warning("action: %s", action)
return action

View File

@ -1,3 +1,11 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_hr_contract_situation_user,hr_contract_situation_user,model_hr_contract_situation,base.group_user,1,0,0,0
access_hr_contract_situation_admin,hr_contract_situation_admin,model_hr_contract_situation,hr_contract.group_hr_contract_manager,1,1,1,1
access_hr_contract_situation_admin,hr_contract_situation_admin,model_hr_contract_situation,hr_contract.group_hr_contract_manager,1,1,1,1
access_sheet_confirmation_wizard_user,sheet_confirmation_wizard_user,model_sheet_confirmation_wizard,base.group_user,1,0,0,0
access_sheet_confirmation_wizard_admin,sheet_confirmation_wizard_admin,model_sheet_confirmation_wizard,hr_contract.group_hr_contract_manager,1,1,1,1
access_amendment_confirmation_wizard_user,amendment_confirmation_wizard_user,model_amendment_confirmation_wizard,base.group_user,1,0,0,0
access_amendment_confirmation_wizard_admin,amendment_confirmation_wizard_admin,model_amendment_confirmation_wizard,hr_contract.group_hr_contract_manager,1,1,1,1
access_sheet_temporality_wizard_user,sheet_temporality_wizard_user,model_sheet_temporality_wizard,base.group_user,1,0,0,0
access_sheet_temporality_wizard_admin,sheet_temporality_wizard_admin,model_sheet_temporality_wizard,hr_contract.group_hr_contract_manager,1,1,1,1
access_sheet_daterange_wizard_user,sheet_daterange_wizard_user,model_sheet_daterange_wizard,base.group_user,1,0,0,0
access_sheet_daterange_wizard_admin,sheet_daterange_wizard_admin,model_sheet_daterange_wizard,hr_contract.group_hr_contract_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_hr_contract_situation_user hr_contract_situation_user model_hr_contract_situation base.group_user 1 0 0 0
3 access_hr_contract_situation_admin hr_contract_situation_admin model_hr_contract_situation hr_contract.group_hr_contract_manager 1 1 1 1
4 access_sheet_confirmation_wizard_user sheet_confirmation_wizard_user model_sheet_confirmation_wizard base.group_user 1 0 0 0
5 access_sheet_confirmation_wizard_admin sheet_confirmation_wizard_admin model_sheet_confirmation_wizard hr_contract.group_hr_contract_manager 1 1 1 1
6 access_amendment_confirmation_wizard_user amendment_confirmation_wizard_user model_amendment_confirmation_wizard base.group_user 1 0 0 0
7 access_amendment_confirmation_wizard_admin amendment_confirmation_wizard_admin model_amendment_confirmation_wizard hr_contract.group_hr_contract_manager 1 1 1 1
8 access_sheet_temporality_wizard_user sheet_temporality_wizard_user model_sheet_temporality_wizard base.group_user 1 0 0 0
9 access_sheet_temporality_wizard_admin sheet_temporality_wizard_admin model_sheet_temporality_wizard hr_contract.group_hr_contract_manager 1 1 1 1
10 access_sheet_daterange_wizard_user sheet_daterange_wizard_user model_sheet_daterange_wizard base.group_user 1 0 0 0
11 access_sheet_daterange_wizard_admin sheet_daterange_wizard_admin model_sheet_daterange_wizard hr_contract.group_hr_contract_manager 1 1 1 1

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="gn_contract_amendment_form_with_situation" model="ir.ui.view">
<field name="name">hr.contract.amendment.form_with_situation</field>
<field name="model">hr.contract.amendment</field>
<field name="inherit_id" ref="gn_contract_amendment.gn_contract_amendment_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='date_start']" position="after">
<field name="situation_ids"/>
</xpath>
</field>
</record>
<record id="view_amendment_confirmation_wizard_form" model="ir.ui.view">
<field name="name">amendment.confirmation.wizard.form</field>
<field name="model">amendment.confirmation.wizard</field>
<field name="arch" type="xml">
<form string="Confirmation">
<field name="message" nolabel="1"/>
<footer>
<button name="confirm_amendment_creation" string="Confirm" type="object" class="btn-primary"/>
<button string="Cancel" class="btn-default" special="cancel"/>
</footer>
</form>
</field>
</record>
</odoo>

View File

@ -9,35 +9,79 @@
</search>
</field>
</record>
<record id="hr_contract_situation_view_tree" model="ir.ui.view">
<field name="name">hr.contract.situation.tree</field>
<field name="model">hr.contract.situation</field>
<field name="arch" type="xml">
<tree string="États des contrats">
<field name="employee_id"/>
<field name="contract_id"/>
<field name="amendment_id"/>
<field name="sheet_id"/>
<field name="date_from"/>
<field name="date_to"/>
<field name="state" widget="label_selection" options="{'classes': {'draft': 'default', 'wait_manager_approval': 'default', 'wait_director_approval': 'default', 'ready': 'success', 'active': 'success', 'expired': 'danger'}}"/>
</tree>
</field>
</record>
<record id="contract_situation_view_form" model="ir.ui.view">
<field name="name">hr.contract.situation.form</field>
<field name="model">hr.contract.situation</field>
<field name="arch" type="xml">
<form string="Current Contract">
<header>
<button name="action_define_sheet" type="object"
groups="hr_contract.group_hr_contract_manager"
string="Définir la fiche de poste" class="oe_highlight"
attrs="{'invisible': [('sheet_id', '!=', False)]}"/>
<button name="action_define_sheet" type="object"
groups="hr_contract.group_hr_contract_manager"
string="Modifier la fiche de poste" class="oe_highlight"
attrs="{'invisible': [('sheet_id', '=', False)]}"/>
<button name="action_define_amendment" type="object"
groups="hr_contract.group_hr_contract_manager"
string="Créer un avenant" class="oe_highlight"
attrs="{'invisible': [('amendment_id', '!=', False)]}"/>
<button name="action_define_amendment" type="object"
groups="hr_contract.group_hr_contract_manager"
string="Modifier l'avenant'" class="oe_highlight"
attrs="{'invisible': [('amendment_id', '=', False)]}"/>
<!--<button name="cron_update_situation_childs_status" type="object"
groups="hr_contract.group_hr_contract_manager"
string="Mettre à jour les états" class="oe_highlight" />-->
<field name="state" groups="!hr_contract.group_hr_contract_manager" widget="statusbar"/>
<field name="state" groups="hr_contract.group_hr_contract_manager" widget="statusbar" options="{'clickable': '1'}"/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
</div>
<div class="oe_title">
<label for="employee_id" class="oe_edit_only"/>
<h1>
<field name="employee_id" placeholder="Employee"/>
</h1>
</div>
<group col="4">
<label for="date_from" string="Period"/>
<div>
<field name="date_from" widget="daterange" class="oe_inline" options="{'related_end_date': 'date_to'}"/>
-
<field name="date_to" widget="daterange" class="oe_inline" options="{'related_start_date': 'date_from'}"/>
<sheet class="mx-1">
<sheet>
<div class="oe_button_box" name="button_box">
</div>
<field name="contract_id" domain="[('employee_id','=',employee_id),('date_start','&lt;=',date_to),'|',('date_end','&gt;=',date_from),('date_end','=',False)]" context="{'default_employee_id': employee_id}"/>
<field name="amendment_id"/>
<field name="sheet_id"/>
</group>
<div class="oe_title">
<label for="employee_id" class="oe_edit_only"/>
<h1>
<field name="employee_id" placeholder="Employee"/>
</h1>
</div>
<group>
<label for="date_from" string="Period"/>
<div>
<field name="date_from" widget="daterange" class="oe_inline" options="{'related_end_date': 'date_to'}"/>
-
<field name="date_to" widget="daterange" class="oe_inline" options="{'related_start_date': 'date_from'}"/>
</div>
<field name="contract_id" attrs="{'readonly': True}" domain="[('employee_id','=',employee_id),('date_start','&lt;=',date_to),'|',('date_end','&gt;=',date_from),('date_end','=',False)]" context="{'default_employee_id': employee_id}"/>
<field name="amendment_id" attrs="{'readonly': True}"/>
<field name="sheet_id" attrs="{'readonly': True}"/>
</group>
</sheet>
<sheet>
<div class="oe_title">
<label for="sheet_name" class="oe_edit_only"/>
<h1>
<field name="sheet_name" placeholder="Employee"/>
</h1>
</div>
</sheet>
</sheet>
</form>
</field>
@ -45,7 +89,7 @@
<record id="action_contract_situation" model="ir.actions.act_window">
<field name="name">Contracts</field>
<field name="res_model">hr.contract.situation</field>
<field name="view_mode">kanban,tree,form,activity</field>
<field name="view_mode">tree,kanban,form,activity</field>
<field name="domain">[('employee_id', '!=', False)]</field>
<field name="context">{'search_default_group_by_state': 1}</field>
<field name="search_view_id" ref="hr_contract_situation_view_search"/>

View File

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="gn_job_sheet_form_with_situation" model="ir.ui.view">
<field name="name">hr.job.sheet.form_with_situation</field>
<field name="model">hr.job.sheet</field>
<field name="inherit_id" ref="gn_job_sheet.gn_job_sheet_form"/>
<field name="arch" type="xml">
<xpath expr="//group[.//field[@name='date_start']]" position="after">
<field name="situation_ids"/>
</xpath>
<xpath expr="//div[@name='state_group']" position="replace">
<field name="state" groups="!hr_contract.group_hr_contract_manager" widget="statusbar"/>
<field name="state" groups="hr_contract.group_hr_contract_manager" widget="statusbar"
options="{'clickable': '1'}"/>
</xpath>
</field>
</record>
<record id="view_sheet_confirmation_wizard_form" model="ir.ui.view">
<field name="name">sheet.confirmation.wizard.form</field>
<field name="model">sheet.confirmation.wizard</field>
<field name="arch" type="xml">
<form string="Confirmation">
<field name="message" nolabel="1"/>
<field name='has_active_id' invisible="1"/>
<field name='has_pending_id' invisible="1"/>
<footer>
<button name="confirm_sheet_creation" string="Nouvelle fiche de poste" type="object" class="btn-primary"
attrs="{'invisible':[('has_pending_id', '=', True)]}"/>
<button name="see_active_sheet" string="Voir la fiche de poste active" type="object" class="btn-primary"
attrs="{'invisible':[('has_active_id', '=', False)]}"/>
<button name="continue_pending_sheet" string="Reprendre la fiche en préparation" type="object" class="btn-primary"
attrs="{'invisible':[('has_pending_id', '=', False)]}"/>
<button string="Cancel" class="btn-default" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="view_sheet_temporality_wizard_form" model="ir.ui.view">
<field name="name">sheet.temporality.wizard.form</field>
<field name="model">sheet.temporality.wizard</field>
<field name="arch" type="xml">
<form string="Temporality">
<field name="message" nolabel="1"/>
<footer>
<button name="is_temporary" string="Créer une fiche de poste temporaire" type="object" class="btn-primary"/>
<button name="is_permanent" string="Remplacer la fiche actuelle" type="object" class="btn-primary"/>
<button string="Cancel" class="btn-default" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="view_sheet_daterange_wizard_form" model="ir.ui.view">
<field name="name">sheet.daterange.wizard.form</field>
<field name="model">sheet.daterange.wizard</field>
<field name="arch" type="xml">
<form string="Temporality">
<field name="message" nolabel="1"/>
<field name='is_permanent' invisible="1"/>
<group name="dates">
<label for="date_start" string="Date de début" attrs="{'invisible':[('is_permanent', '=', False)]}"/>
<label for="date_start" string="Période" attrs="{'invisible':[('is_permanent', '=', True)]}"/>
<div attrs="{'invisible':[('is_permanent', '=', False)]}">
<field name="date_start"/>
</div>
<div attrs="{'invisible':[('is_permanent', '=', True)]}">
<field name="date_start" widget="daterange" class="oe_inline" options="{'related_end_date': 'date_end'}"/>
-
<field name="date_end" widget="daterange" class="oe_inline" options="{'related_start_date': 'date_start'}"/>
</div>
</group>
<footer>
<button name="confirm_dates" string="Valider la date de début" type="object" class="btn-primary" attrs="{'invisible':[('is_permanent', '=', False)]}"/>
<button name="confirm_dates" string="Valider la période" type="object" class="btn-primary" attrs="{'invisible':[('is_permanent', '=', True)]}"/>
<button string="Cancel" class="btn-default" special="cancel"/>
</footer>
</form>
</field>
</record>
</odoo>