Commit b8ea7bbc by François C.

Merge branch 'evolution_bdm' into dev_cooperatic

parents a5b7181d 0ebfe020
...@@ -660,7 +660,7 @@ msgstr "Remplacé" ...@@ -660,7 +660,7 @@ msgstr "Remplacé"
#: code:addons/coop_membership/models/shift_shift.py:81 #: code:addons/coop_membership/models/shift_shift.py:81
#, python-format #, python-format
msgid "Shift Cloture" msgid "Shift Cloture"
msgstr "Clôlturer le service" msgstr "Clôturer le service"
#. module: coop_membership #. module: coop_membership
#: model:ir.ui.view,arch_db:coop_membership.view_shift_form_inherit #: model:ir.ui.view,arch_db:coop_membership.view_shift_form_inherit
......
...@@ -2297,7 +2297,7 @@ msgstr "Service" ...@@ -2297,7 +2297,7 @@ msgstr "Service"
#: code:addons/coop_shift/model/shift_shift.py:434 #: code:addons/coop_shift/model/shift_shift.py:434
#, python-format #, python-format
msgid "Shift Cloture" msgid "Shift Cloture"
msgstr "Clôlturer le service" msgstr "Clôturer le service"
#. module: coop_shift #. module: coop_shift
#: model:ir.actions.act_window,name:coop_shift.action_res_partner_point_counter_view_ftop #: model:ir.actions.act_window,name:coop_shift.action_res_partner_point_counter_view_ftop
......
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
:alt: License: AGPL-3
=========================
La Cagette specific rules
=========================
All shift template are FTOP type.
Every cooperative members is subscribing a FTOP shift template.
Every 28 days (4 weeks), every shift template subscriber losts 1 point.
If a cooperative member is not present to a due shift, she or he losts 1 point and has 2 makeups to do.
Every cooperative member move its regular shift to an other day.
When the coop. member counter reaches -2 points, the cooperative status is set to 'Suspended'.
When the coop. member counter reaches -3 points, the cooperatice status is set to 'Unsubscribed'
# -*- coding: utf-8 -*-
from . import models
\ No newline at end of file
# -*- coding: utf-8 -*-
{
'name': "La Cagette - Membership",
'summary': """
Tuning membership rules""",
'description': """
Specific rules are beeing implemented
lacagette_membership.absence_status can be excused or absent
""",
'author': "fracolo/cooperatic",
'website': "https://lacagette-coop.fr",
#
'category': 'Uncategorized',
'version': '0.0.6',
# any module necessary for this one to work correctly
'depends': ['base', 'coop_membership', 'coop_shift'],
# always loaded
'data': [
'security/ir.model.access.csv',
'data/ir_cron.xml',
'data/ir_config_parameter_data.xml',
'static/src/xml/templates.xml',
'views/res_partner.xml',
],
'installable': True,
}
<?xml version="1.0"?>
<odoo noupdate="1">
<record id="points_limit_to_get_suspended" model="ir.config_parameter">
<field name="key">lacagette_membership.points_limit_to_get_suspended</field>
<field name="value">-2</field>
</record>
<record id="points_limit_to_get_unsubscribed" model="ir.config_parameter">
<field name="key">lacagette_membership.points_limit_to_get_unsubscribed</field>
<field name="value">-3</field>
</record>
<record id="makeups_to_do_after_unsubscribed" model="ir.config_parameter">
<field name="key">lacagette_membership.makeups_to_do_after_unsubscribed</field>
<field name="value">2</field>
</record>
<record id="absence_status" model="ir.config_parameter">
<field name="key">lacagette_membership.absence_status</field>
<field name="value">excused</field>
</record>
<record id="extension_duration" model="ir.config_parameter">
<field name="key">lacagette_membership.extension_duration</field>
<field name="value">6 months</field>
</record>
<record id="max_successive_extensions" model="ir.config_parameter">
<field name="key">lacagette_membership.max_successive_extensions</field>
<field name="value">1</field>
</record>
<record id="committees_shift_id" model="ir.config_parameter">
<field name="key">lacagette_membership.committees_shift_id</field>
<field name="value">0</field>
</record>
<record id="shift_creation_days" model="ir.config_parameter">
<field name="key">lacagette_membership.shift_creation_days</field>
<field name="value">180</field>
</record>
</odoo>
<?xml version="1.0"?>
<!--
Copyright (C) 2021 - Today Cooperatic
Author : fracolo
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
-->
<odoo noupdate="1">
<record forcecreate="True" id="cron_close_unnecessary_extensions" model="ir.cron">
<field name="name">Close unnecessary Extensions</field>
<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="doall" eval="True"/>
<field name="model" eval="'res.partner'"/>
<field name="function" eval="'run_close_unnecessary_opened_extensions'"/>
<field name="args" eval="'()'"/>
<field name="active" eval="False"/>
<field name="priority">2</field>
</record>
<record forcecreate="True" id="cron_process_target_status" model="ir.cron">
<field name="name">Process coop target status</field>
<field name="user_id" ref="base.user_root" />
<field name="interval_number">5</field>
<field name="interval_type">minutes</field>
<field name="numbercall">-1</field>
<field name="doall" eval="True"/>
<field name="model" eval="'res.partner'"/>
<field name="function" eval="'run_process_target_status'"/>
<field name="args" eval="'()'"/>
<field name="active" eval="False"/>
<field name="priority">1</field>
</record>
<record forcecreate="True" id="cron_close_ftop_shifts" model="ir.cron">
<field name="name">Daily close FTOP shifts</field>
<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="doall" eval="True"/>
<field name="model" eval="'shift.shift'"/>
<field name="function" eval="'run_close_today_ftop_shifts'"/>
<field name="args" eval="'()'"/>
<field name="active" eval="False"/>
<field name="priority">1</field>
</record>
<record forcecreate="True" id="cron_shifts_generation" model="ir.cron">
<field name="name">Generate shifts (La Cagette version)</field>
<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="doall" eval="False"/>
<field name="model" eval="'lacagette.shift.template'"/>
<field name="function" eval="'run_shift_creation'"/>
<field name="args" eval="'()'"/>
<field name="active" eval="False"/>
<field name="priority">1</field>
</record>
</odoo>
\ No newline at end of file
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * lacagette_membership
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo 9.0\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Language: fr\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#. module: lacagette_membership
#: selection:res.partner,cooperative_state:0
msgid "Gone"
msgstr "Parti.e"
#. module: lacagette_membership
#: selection:res.partner,cooperative_state:0
msgid "Associated"
msgstr "En binôme"
\ No newline at end of file
# -*- coding: utf-8 -*-
from . import res_partner
from . import shift_registration
from . import shift_counter_event
from . import shift_shift
from . import member_state_change
from . import shift_template
from . import shift_template_registration_line
# -*- coding: utf-8 -*-
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html
from openerp import fields, models
class MembersStateChange(models.Model):
_name = "member.state.change"
member_id = fields.Many2one(
comodel_name='res.partner',
ondelete='cascade',
index=True
)
state = fields.Char(
'state',
size=15
)
# -*- coding: utf-8 -*-
from openerp import _, api, models, fields
import datetime
import logging
_logger = logging.getLogger(__name__)
TARGET_STATUS_SELECTION = [
('unsubscribed', 'Unsubscribed'),
('suspended', 'Suspended')
]
EXTRA_COOPERATIVE_STATE_SELECTION = [
('not_concerned', 'Not Concerned'),
('unsubscribed', 'Unsubscribed'),
('exempted', 'Exempted'),
('vacation', 'On Vacation'),
('up_to_date', 'Up to date'),
('alert', 'Alert'),
('suspended', 'Suspended'),
('delay', 'Delay'),
('blocked', 'Blocked'),
('unpayed', 'Unpayed'),
('gone', 'Gone'),
('associated', 'Associated')
]
class ResPartner(models.Model):
_inherit = 'res.partner'
# Columns Section
makeups_to_do = fields.Integer(
"Number of make-ups to done",
default= 0)
target_status = fields.Selection(
selection=TARGET_STATUS_SELECTION, default='')
cooperative_state = fields.Selection(
selection=EXTRA_COOPERATIVE_STATE_SELECTION, default='not_concerned')
@api.model
def run_process_target_status(self):
"""Method called by cron task"""
# final_ftop_point, target_status
conf = self.env['ir.config_parameter']
makeups_todo_after_unsubscribed = conf.get_param("lacagette_membership.makeups_to_do_after_unsubscribed")
result = 'done'
for p in self.env['res.partner']\
.search([('target_status', '!=', "")]):
try:
new_values = {'target_status': "", "date_alert_stop": False}
final_points = p.final_ftop_point if p.shift_type == "ftop" else p.final_standard_point
if final_points < 0:
new_values['cooperative_state'] = p.target_status
if new_values['cooperative_state'] == "unsubscribed":
new_values['makeups_to_do'] = makeups_todo_after_unsubscribed
new_values['final_' + p.shift_type + '_point'] = -2
if p.shift_type == 'standard':
new_values['display_std_points'] = -2
else:
new_values['display_ftop_points'] = -2
"""
unlink model: "shift.template.registration"
to delete all future shifts linked to this coop.
"""
now = datetime.datetime.now().isoformat()
for streg in self.env['shift.template.registration']\
.search([('partner_id', '=', p.id)]):
streg.unlink()
for sreg in self.env['shift.registration']\
.search([('partner_id', '=', p.id),
('date_begin', '>', now)]):
sreg.unlink()
# Close extensions
for ext in self.env['shift.extension']\
.search([('partner_id', '=', p.id),
('date_start', '<=', now),
('date_stop', '>=', now)]):
ext.update({'date_stop': now})
mail_template = self.env.ref('coop_membership.unsubscribe_email')
if mail_template:
mail_template.send_mail(p.id)
p.update(new_values)
except Exception as e:
_logger.error("run_process_target_status : %s", str(e))
result = 'error'
return result
#@api.onchange('cooperativestate') : could be used only if it is called from client
@api.model
def run_close_unnecessary_opened_extensions(self):
"""Method called by cron task"""
sql = """
SELECT s.id
FROM shift_extension as s
WHERE s.date_stop > now()
AND partner_id IN (SELECT id FROM res_partner WHERE cooperative_state in ('up_to_date', 'unsubscribed'))
"""
self.env.cr.execute(sql)
extension_ids = self.env.cr.fetchall()
if len(extension_ids) > 0:
extension_model = self.env['shift.extension']
today = datetime.datetime.now().strftime("%Y-%m-%d")
for ext in extension_model.search([('id', 'in', extension_ids)]):
ext.update({'date_stop': today})
@api.multi
def can_have_extension(self):
"""Return if the member can ask for an extension"""
answer = True
conf = self.env['ir.config_parameter']
max_nb = int(conf.get_param("lacagette_membership.max_successive_extensions"))
if max_nb == 1:
args = [('member_id', '=', self.id)]
states = self.env['member.state.change']\
.search(args, order='write_date DESC', limit=2)
"""
member.state.change may have no record for the current member
"""
if len(states) > 0:
if (len(states) == 1 and states[0]['state'] == 'delay'
or
len(states) == 2 and states[1]['state'] == 'delay'):
answer = False
else:
# need to load data
member = self.env['res.partner'].search([('id', '=', self.id)])[0]
# should exist an other way to do it , but haven't found it
if member.cooperative_state != "up_to_date":
answer = False
else:
# TODO : Must have a look to previous extensions
pass
return answer
def set_special_state(self, cr, uid, partner, context=None):
return self.write(cr, uid, [partner['id']], {'cooperative_state': partner['state']} , context=context)
def _write_state_change(self, state):
data = {'member_id': self.id, 'state': state}
self.env['member.state.change'].create(data)
@api.multi
def update(self, vals):
_logger.info("valeurs reçues pour update partner = %s", str(vals))
state_to_record = ""
if 'cooperative_state' in vals:
state_to_record = vals['cooperative_state']
elif 'current_cooperative_state' in vals:
state_to_record = vals['current_cooperative_state']
del vals['current_cooperative_state']
if len(state_to_record) > 0:
self._write_state_change(state_to_record)
return super(ResPartner, self).update(vals)
@api.multi
def write(self, vals):
_logger.info("valeurs reçues pour write partner = %s", str(vals))
if 'cooperative_state' in vals:
self._write_state_change(vals['cooperative_state'])
return super(ResPartner, self).write(vals)
# -*- coding: utf-8 -*-
from openerp import _, api, models, fields
import logging
_logger = logging.getLogger(__name__)
class ShiftCounterEvent(models.Model):
_inherit = 'shift.counter.event'
def _update_partner_target_status(self, vals):
"""actions if point_qty is negative"""
conf = self.env['ir.config_parameter']
suspension_limit = int(conf.get_param("lacagette_membership.points_limit_to_get_suspended"))
unsubscribe_limit = int(conf.get_param("lacagette_membership.points_limit_to_get_unsubscribed"))
if vals['point_qty'] < 0:
res_partner = self.env['res.partner'].search([('id', '=', vals['partner_id'])])
if res_partner:
p = res_partner[0]
points_before_removing_points = p.final_ftop_point if p.shift_type == "ftop" else p.final_standard_point
points_after_removal = points_before_removing_points + vals['point_qty']
_logger.info("points_after_removal = %s, current_state = %s", str(points_after_removal), p.cooperative_state)
if points_after_removal <= suspension_limit or points_after_removal <= unsubscribe_limit:
target_status = None
if (points_after_removal <= suspension_limit
and
p.cooperative_state != 'delay'):
target_status = 'suspended'
if points_after_removal <= unsubscribe_limit:
target_status = 'unsubscribed'
if target_status is not None:
p.update({'target_status': target_status,
'current_cooperative_state': p.cooperative_state})
@api.model
def write(self, vals):
res = super(ShiftCounterEvent, self).write(vals)
_logger.info("Vals reçues = %s", str(vals))
self._update_partner_target_status(vals)
return res
@api.model
def create(self, vals):
self._update_partner_target_status(vals)
_logger.info("Vals reçues creation = %s", str(vals))
return super(ShiftCounterEvent, self).create(vals)
# -*- coding: utf-8 -*-
from openerp import _, api, models, fields
import logging
_logger = logging.getLogger(__name__)
class ShiftRegistration(models.Model):
_inherit = 'shift.registration'
is_late = fields.Boolean(
"Was the registration validated within grace period ?",
default= False)
is_makeup = fields.Boolean(
"Is this registration a consequence of a makeup to do",
default= False)
@api.multi
def write(self, vals):
conf = self.env['ir.config_parameter']
absence_status = conf.get_param("lacagette_membership.absence_status")
if 'state' in vals and vals['state'] == absence_status:
if self.ids:
# it is the case when called from "absence cron job" run in external third-party
to_add = 2 if absence_status == 'absent' else 1
for s in self.env['shift.registration']\
.search([('id', 'in', self.ids)]):
new_makeups_to_do = s.partner_id.makeups_to_do + to_add
s.partner_id.update({'makeups_to_do': new_makeups_to_do})
return super(ShiftRegistration, self).write(vals)
\ No newline at end of file
# -*- coding: utf-8 -*-
from openerp import _, api, models, fields
import logging
_logger = logging.getLogger(__name__)
class ShiftShift(models.Model):
_inherit = 'shift.shift'
@api.model
def run_close_today_ftop_shifts(self):
"""Method called by cron task"""
sql = """
SELECT id
FROM shift_shift
WHERE TO_CHAR(date_begin, 'YYYY-MM-DD') = TO_CHAR(now(), 'YYYY-MM-DD')
AND active = true AND state = 'confirm'
"""
self.env.cr.execute(sql)
shift_ids = self.env.cr.fetchall()
if len(shift_ids) > 0:
for s in self.env['shift.shift'].search([('id', 'in', shift_ids)]):
s.button_makeupok()
s.button_done()
# _logger.info('Traité avec action makeupok et done sur shift ' + str(s.id))
\ No newline at end of file
# -*- coding: utf-8 -*-
from openerp import _, api, models, fields
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
import locale
import logging
_logger = logging.getLogger(__name__)
class ShiftTemplate(models.Model):
_name = "lacagette.shift.template"
_inherit = 'shift.template'
@api.model
def ng_create_shifts_from_template(self, instance=None, after=False, before=False):
conf = self.env['ir.config_parameter']
shift_creation_days = int(conf.get_param("lacagette_membership.shift_creation_days"))
if not before:
before = fields.Datetime.to_string(
datetime.today() + timedelta(days=shift_creation_days))
for template in instance:
if len(str(template.rrule).strip()) == 0:
continue # It's not a recurring template
after = template.last_shift_date
rec_dates = template.get_recurrent_dates(
after=after, before=before)
for rec_date in rec_dates:
start_date_object_tz = datetime.strptime(
template.start_datetime_tz, '%Y-%m-%d %H:%M:%S')
date_begin = datetime.strftime(
rec_date + timedelta(hours=(start_date_object_tz.hour)) +
timedelta(minutes=(start_date_object_tz.minute)),
"%Y-%m-%d %H:%M:%S")
if date_begin.split(" ")[0] <= template.last_shift_date:
continue
end_date_object_tz = datetime.strptime(
template.end_datetime_tz, '%Y-%m-%d %H:%M:%S')
diff_day = end_date_object_tz.day - start_date_object_tz.day
diff_month = end_date_object_tz.month -\
start_date_object_tz.month
diff_year = end_date_object_tz.year - start_date_object_tz.year
date_end = datetime.strftime(
rec_date + timedelta(hours=(end_date_object_tz.hour)) +
timedelta(minutes=(end_date_object_tz.minute)) +
relativedelta(days=diff_day) +
relativedelta(months=diff_month) +
relativedelta(years=diff_year),
"%Y-%m-%d %H:%M:%S")
rec_date = datetime.strftime(rec_date, "%Y-%m-%d")
vals = {
'shift_template_id': template.id,
'name': template.name,
'user_ids': [(6, 0, template.user_ids.ids)],
'company_id': template.company_id.id,
'seats_max': template.seats_max,
'seats_availability': template.seats_availability,
'seats_min': template.seats_min,
'date_begin_tz': date_begin,
'date_end_tz': date_end,
'state': 'draft',
'reply_to': template.reply_to,
'address_id': template.address_id.id,
'description': template.description,
'shift_type_id': template.shift_type_id.id,
'week_number': template.week_number,
'week_list': template.week_list,
'shift_ticket_ids': None,
}
shift_id = self.env['shift.shift'].create(vals)
for ticket in template.shift_ticket_ids:
vals = {
'name': ticket.name,
'shift_id': shift_id.id,
'product_id': ticket.product_id.id,
'price': ticket.price,
'deadline': ticket.deadline,
'seats_availability': ticket.seats_availability,
'seats_max': ticket.seats_max,
}
if ticket.product_id.shift_type_id.is_ftop:
vals['seats_availability'] = 'limited'
vals['seats_max'] = 0
ticket_id = self.env['shift.ticket'].create(vals)
for attendee in ticket.registration_ids:
state, strl_id = attendee._get_state(rec_date)
if state:
vals = {
'partner_id': attendee.partner_id.id,
'user_ids': [(6, 0, template.user_ids.ids)],
'state': state,
'email': attendee.email,
'phone': attendee.phone,
'name': attendee.name,
'shift_id': shift_id.id,
'shift_ticket_id': ticket_id.id,
'tmpl_reg_line_id': strl_id,
'template_created': True,
}
self.env['shift.registration'].with_context(
from_shift_template=True).create(vals)
@api.model
def run_shift_creation(self):
# This method is called by the cron task
locale.setlocale(locale.LC_TIME, 'en_US.UTF-8')
conf = self.env['ir.config_parameter']
shift_creation_days = int(conf.get_param("lacagette_membership.shift_creation_days"))
today = datetime.today()
futur = today + timedelta(days=shift_creation_days)
filters = [('recurrency', '=', True),
('week_list', '=', today.strftime('%a').upper()[0:2]),
('last_shift_date', '<=', datetime.strftime(futur, "%Y-%m-%d"))]
templates = self.env['shift.template'].search(filters)
# templates = self.env['shift.template'].search([('id', '=', 60)])
for template in templates:
# _logger.info("Shift template = %s", str(template))
self.ng_create_shifts_from_template( instance=template,
before=fields.Datetime.to_string(
datetime.today() + timedelta(days=shift_creation_days)))
\ No newline at end of file
# -*- coding: utf-8 -*-
from openerp import api, models
import logging
_logger = logging.getLogger(__name__)
class ShiftTemplateRegistrationLine(models.Model):
_inherit = 'shift.template.registration.line'
@api.model
def create(self, vals):
super_res = super(ShiftTemplateRegistrationLine, self.with_context(
dict(self.env.context, **{'creation_in_progress': True}))).create(vals)
#_logger.info("valeurs reçues pour create shift.template.registration.line = %s", str(vals))
if 'partner_id' in vals and 'shift_ticket_id' in vals:
conf = self.env['ir.config_parameter']
suspension_limit = int(conf.get_param("lacagette_membership.points_limit_to_get_suspended"))
unsubscribe_limit = int(conf.get_param("lacagette_membership.points_limit_to_get_unsubscribed"))
member = self.env['res.partner'].search([('id', '=', vals['partner_id'])])
shift_ticket = self.env['shift.ticket'].search([('id', '=', vals['shift_ticket_id'])])
if member and shift_ticket:
member = member[0]
shift_ticket = shift_ticket[0]
if shift_ticket.name == "ABCD":
points = member.final_standard_point
else:
points = member.final_ftop_point
if points <= suspension_limit or points <= unsubscribe_limit:
coop_state = 'suspended'
if points <= unsubscribe_limit:
coop_state = 'unsubscribed'
member.update({'cooperative_state': coop_state})
_logger.info("new cooperative_state = %s for %s", coop_state, str(member.id))
return super_res
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
,,,,,,,
,,,,,,,
access_model_member_state_change,access_model_member_state_change,model_member_state_change,coop_shift.group_shift_user,1,1,1,1
#coop_state_selection {
font-size: medium;
height: 26px;
}
\ No newline at end of file
odoo.define('lacagette_membership.backend', function(require){
"use strict";
var core = require('web.core');
var FormView = require('web.FormView');
var Model = require('web.DataModel');
var framework = require('web.framework');
var _t = core._t;
var partner_id = null;
var last_call = 0;
FormView.include({
load_record: function() {
var self = this;
return this._super.apply(this, arguments)
.then(function() {
partner_id = self.get_fields_values().id;
});
}
});
$(document).on('change', '#coop_state_selection', function(){
var d = new Date()
var elapsed_since_last_call = d.getTime() - last_call
let new_state = $(this).val()
if (elapsed_since_last_call > 5000) {
last_call = d.getTime()
let fields = {id: partner_id, state: new_state}
framework.blockUI();
new Model('res.partner').call('set_special_state',[fields]).then(function(rData){
window.location.reload()
},function(err,event){
event.preventDefault();
var error_body = _t('Your Internet connection is probably down.');
if (err.data) {
var except = err.data;
error_body = except.arguments && except.arguments[0] || except.message || error_body;
}
alert(error_body)
});
}
});
});
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<template id="assets" name="lacagette_membership assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<link rel="stylesheet" href="/lacagette_membership/static/src/css/backend.css" />
<script type="text/javascript" src="/lacagette_membership/static/src/js/backend.js"></script>
</xpath>
</template>
</data>
</openerp>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_partner_form" model="ir.ui.view">
<field name="model">res.partner</field>
<field name="inherit_id" ref="coop_membership.view_partner_form" />
<field name="arch" type="xml">
<field name="name" position="after">
<select id="coop_state_selection">
<option value="">--Statuts spéciaux--</option>
<option value="gone">Parti.e</option>
<option value="associated">En binôme</option>
</select>
</field>
</field>
</record>
</odoo>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment