mirror of
https://github.com/OCA/pms.git
synced 2025-01-29 00:17:45 +02:00
[ADD] pms: wizard massive changes on availability plans and pricelists (#28)
* [IMP] pms: wizard massive changes on availability plans * [TEST] add creation rule based on weeek days * [REF] pms: model av. plan, rule_ids & inv. name * [IMP] pms: wizard called from pricelist & availability plans consistently * [IMP] pms: wizard massive changes on pricelists * [FIX] pms: fix creation pricelist items * [FIX] pms: fix timezone on create pricelist items * [TEST] pms: fix time zone issues in testing
This commit is contained in:
@@ -1 +1,2 @@
|
||||
from . import wizard_reservation
|
||||
from . import wizard_massive_changes
|
||||
|
||||
392
pms/wizards/wizard_massive_changes.py
Normal file
392
pms/wizards/wizard_massive_changes.py
Normal file
@@ -0,0 +1,392 @@
|
||||
import datetime
|
||||
|
||||
import pytz
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class AvailabilityWizard(models.TransientModel):
|
||||
|
||||
_name = "pms.massive.changes.wizard"
|
||||
_description = "Wizard for massive changes on Availability Plans & Pricelists."
|
||||
|
||||
# Fields declaration
|
||||
massive_changes_on = fields.Selection(
|
||||
[("pricelist", "Pricelist"), ("availability_plan", "Availability Plan")],
|
||||
string="Massive changes on",
|
||||
default="availability_plan",
|
||||
required=True,
|
||||
)
|
||||
availability_plan_id = fields.Many2one(
|
||||
comodel_name="pms.room.type.availability.plan",
|
||||
string="Availability Plan to apply massive changes",
|
||||
# can be setted by context from availability plan detail
|
||||
)
|
||||
pricelist_id = fields.Many2one(
|
||||
comodel_name="product.pricelist",
|
||||
string="Pricelist to apply massive changes",
|
||||
)
|
||||
allowed_pricelist_ids = fields.One2many(
|
||||
comodel_name="product.pricelist", compute="_compute_allowed_pricelist_ids"
|
||||
)
|
||||
start_date = fields.Date(
|
||||
string="From:",
|
||||
required=True,
|
||||
)
|
||||
end_date = fields.Date(
|
||||
string="To:",
|
||||
required=True,
|
||||
)
|
||||
room_type_id = fields.Many2one(comodel_name="pms.room.type", string="Room Type")
|
||||
price = fields.Float(string="Price")
|
||||
min_quantity = fields.Float(string="Min. Quantity")
|
||||
|
||||
min_stay = fields.Integer(
|
||||
string="Min. Stay",
|
||||
default=0,
|
||||
)
|
||||
min_stay_arrival = fields.Integer(
|
||||
string="Min. Stay Arrival",
|
||||
default=0,
|
||||
)
|
||||
max_stay = fields.Integer(
|
||||
string="Max. Stay",
|
||||
default=0,
|
||||
)
|
||||
max_stay_arrival = fields.Integer(
|
||||
string="Max. Stay Arrival",
|
||||
default=0,
|
||||
)
|
||||
closed = fields.Boolean(
|
||||
string="Closed",
|
||||
default=False,
|
||||
)
|
||||
closed_departure = fields.Boolean(
|
||||
string="Closed Departure",
|
||||
default=False,
|
||||
)
|
||||
closed_arrival = fields.Boolean(
|
||||
string="Closed Arrival",
|
||||
default=False,
|
||||
)
|
||||
quota = fields.Integer(
|
||||
string="Quota",
|
||||
help="Generic Quota assigned.",
|
||||
default=-1,
|
||||
)
|
||||
max_avail = fields.Integer(
|
||||
string="Max. Availability",
|
||||
help="Maximum simultaneous availability on own Booking Engine.",
|
||||
default=-1,
|
||||
)
|
||||
apply_on_monday = fields.Boolean(
|
||||
string="Apply Availability Rule on mondays",
|
||||
default=False,
|
||||
)
|
||||
apply_on_tuesday = fields.Boolean(
|
||||
string="Apply Availability Rule on tuesdays",
|
||||
default=False,
|
||||
)
|
||||
apply_on_wednesday = fields.Boolean(
|
||||
string="Apply Availability Rule on wednesdays",
|
||||
default=False,
|
||||
)
|
||||
apply_on_thursday = fields.Boolean(
|
||||
string="Apply Availability Rule on thursdays",
|
||||
default=False,
|
||||
)
|
||||
apply_on_friday = fields.Boolean(
|
||||
string="Apply Availability Rule on fridays",
|
||||
default=False,
|
||||
)
|
||||
apply_on_saturday = fields.Boolean(
|
||||
string="Apply Availability Rule on saturdays",
|
||||
default=False,
|
||||
)
|
||||
apply_on_sunday = fields.Boolean(
|
||||
string="Apply Availability Rule on sundays",
|
||||
default=False,
|
||||
)
|
||||
apply_on_all_week = fields.Boolean(
|
||||
string="Apply Availability Rule for the whole week",
|
||||
default=True,
|
||||
)
|
||||
rules_to_overwrite = fields.One2many(
|
||||
comodel_name="pms.room.type.availability.rule",
|
||||
compute="_compute_rules_to_overwrite",
|
||||
store=False,
|
||||
readonly=True,
|
||||
)
|
||||
pricelist_items_to_overwrite = fields.One2many(
|
||||
comodel_name="product.pricelist.item",
|
||||
compute="_compute_pricelist_items_to_overwrite",
|
||||
store=False,
|
||||
readonly=True,
|
||||
)
|
||||
num_rules_to_overwrite = fields.Integer(
|
||||
string="Rules to overwrite on massive changes",
|
||||
compute="_compute_num_rules_to_overwrite",
|
||||
store=False,
|
||||
readonly=True,
|
||||
)
|
||||
num_pricelist_items_to_overwrite = fields.Integer(
|
||||
string="Pricelist items to overwrite on massive changes",
|
||||
compute="_compute_num_pricelist_items_to_overwrite",
|
||||
store=False,
|
||||
readonly=True,
|
||||
)
|
||||
avail_readonly = fields.Boolean(compute="_compute_avail_readonly")
|
||||
pricelist_readonly = fields.Boolean(compute="_compute_pricelist_readonly")
|
||||
|
||||
def _compute_allowed_pricelist_ids(self):
|
||||
for record in self:
|
||||
record.allowed_pricelist_ids = self.env["product.pricelist"].search(
|
||||
[
|
||||
("pricelist_type", "=", "daily"),
|
||||
]
|
||||
)
|
||||
|
||||
@api.depends(
|
||||
"start_date",
|
||||
"end_date",
|
||||
"room_type_id",
|
||||
"apply_on_monday",
|
||||
"apply_on_tuesday",
|
||||
"apply_on_wednesday",
|
||||
"apply_on_thursday",
|
||||
"apply_on_friday",
|
||||
"apply_on_saturday",
|
||||
"apply_on_sunday",
|
||||
"apply_on_all_week",
|
||||
"availability_plan_id",
|
||||
)
|
||||
def _compute_rules_to_overwrite(self):
|
||||
for record in self:
|
||||
|
||||
if not record.availability_plan_id and self._context.get(
|
||||
"availability_plan_id"
|
||||
):
|
||||
record.availability_plan_id = self._context.get("availability_plan_id")
|
||||
record.massive_changes_on = "availability_plan"
|
||||
|
||||
if record.availability_plan_id:
|
||||
domain = [
|
||||
("availability_plan_id", "=", record.availability_plan_id.id),
|
||||
]
|
||||
|
||||
if record.room_type_id:
|
||||
domain.append(("room_type_id", "=", record.room_type_id.id))
|
||||
if record.start_date:
|
||||
domain.append(("date", ">=", record.start_date))
|
||||
if record.end_date:
|
||||
domain.append(("date", "<=", record.end_date))
|
||||
|
||||
week_days_to_apply = (
|
||||
record.apply_on_monday,
|
||||
record.apply_on_tuesday,
|
||||
record.apply_on_wednesday,
|
||||
record.apply_on_thursday,
|
||||
record.apply_on_friday,
|
||||
record.apply_on_saturday,
|
||||
record.apply_on_sunday,
|
||||
)
|
||||
if record.start_date and record.end_date:
|
||||
rules = self.env["pms.room.type.availability.rule"].search(domain)
|
||||
if (
|
||||
not record.apply_on_all_week
|
||||
and record.start_date
|
||||
and record.end_date
|
||||
):
|
||||
record.rules_to_overwrite = rules.filtered(
|
||||
lambda x: week_days_to_apply[x.date.timetuple()[6]]
|
||||
)
|
||||
else:
|
||||
record.rules_to_overwrite = rules
|
||||
else:
|
||||
record.rules_to_overwrite = False
|
||||
else:
|
||||
record.rules_to_overwrite = False
|
||||
|
||||
@api.depends(
|
||||
"start_date",
|
||||
"end_date",
|
||||
"room_type_id",
|
||||
"apply_on_monday",
|
||||
"apply_on_tuesday",
|
||||
"apply_on_wednesday",
|
||||
"apply_on_thursday",
|
||||
"apply_on_friday",
|
||||
"apply_on_saturday",
|
||||
"apply_on_sunday",
|
||||
"apply_on_all_week",
|
||||
"pricelist_id",
|
||||
)
|
||||
def _compute_pricelist_items_to_overwrite(self):
|
||||
for record in self:
|
||||
|
||||
if not record.pricelist_id and self._context.get("pricelist_id"):
|
||||
record.pricelist_id = self._context.get("pricelist_id")
|
||||
record.massive_changes_on = "pricelist"
|
||||
|
||||
if record.pricelist_id:
|
||||
domain = [
|
||||
("pricelist_id", "=", record.pricelist_id.id),
|
||||
]
|
||||
|
||||
if record.start_date:
|
||||
domain.append(("date_start", ">=", record.start_date))
|
||||
|
||||
if record.end_date:
|
||||
domain.append(("date_end", "<=", record.end_date))
|
||||
if record.room_type_id:
|
||||
domain.append(
|
||||
(
|
||||
"product_tmpl_id",
|
||||
"=",
|
||||
record.room_type_id.product_id.product_tmpl_id.id,
|
||||
)
|
||||
)
|
||||
|
||||
week_days_to_apply = (
|
||||
record.apply_on_monday,
|
||||
record.apply_on_tuesday,
|
||||
record.apply_on_wednesday,
|
||||
record.apply_on_thursday,
|
||||
record.apply_on_friday,
|
||||
record.apply_on_saturday,
|
||||
record.apply_on_sunday,
|
||||
)
|
||||
|
||||
if record.start_date and record.end_date:
|
||||
items = self.env["product.pricelist.item"].search(domain)
|
||||
if (
|
||||
not record.apply_on_all_week
|
||||
and record.start_date
|
||||
and record.end_date
|
||||
):
|
||||
record.pricelist_items_to_overwrite = items.filtered(
|
||||
lambda x: week_days_to_apply[x.date_start.timetuple()[6]]
|
||||
)
|
||||
else:
|
||||
record.pricelist_items_to_overwrite = items
|
||||
else:
|
||||
record.pricelist_items_to_overwrite = False
|
||||
else:
|
||||
record.pricelist_items_to_overwrite = False
|
||||
|
||||
@api.depends(
|
||||
"rules_to_overwrite",
|
||||
)
|
||||
def _compute_num_rules_to_overwrite(self):
|
||||
for record in self:
|
||||
self.num_rules_to_overwrite = len(record.rules_to_overwrite)
|
||||
|
||||
@api.depends(
|
||||
"pricelist_items_to_overwrite",
|
||||
)
|
||||
def _compute_num_pricelist_items_to_overwrite(self):
|
||||
for record in self:
|
||||
self.num_pricelist_items_to_overwrite = len(
|
||||
record.pricelist_items_to_overwrite
|
||||
)
|
||||
|
||||
def _compute_avail_readonly(self):
|
||||
for record in self:
|
||||
record.avail_readonly = (
|
||||
True if self._context.get("availability_plan_id") else False
|
||||
)
|
||||
|
||||
def _compute_pricelist_readonly(self):
|
||||
for record in self:
|
||||
record.pricelist_readonly = (
|
||||
True if self._context.get("pricelist_id") else False
|
||||
)
|
||||
|
||||
# actions
|
||||
def apply_massive_changes(self):
|
||||
tz = "Europe/Madrid"
|
||||
for record in self:
|
||||
# remove old rules
|
||||
record.rules_to_overwrite.unlink()
|
||||
record.pricelist_items_to_overwrite.unlink()
|
||||
week_days_to_apply = (
|
||||
record.apply_on_monday,
|
||||
record.apply_on_tuesday,
|
||||
record.apply_on_wednesday,
|
||||
record.apply_on_thursday,
|
||||
record.apply_on_friday,
|
||||
record.apply_on_saturday,
|
||||
record.apply_on_sunday,
|
||||
)
|
||||
|
||||
# dates between start and end (both included)
|
||||
for date in [
|
||||
record.start_date + datetime.timedelta(days=x)
|
||||
for x in range(0, (record.end_date - record.start_date).days + 1)
|
||||
]:
|
||||
|
||||
if (
|
||||
not record.apply_on_all_week
|
||||
and not week_days_to_apply[date.timetuple()[6]]
|
||||
):
|
||||
continue
|
||||
|
||||
if not record.room_type_id:
|
||||
rooms = self.env["pms.room.type"].search([])
|
||||
else:
|
||||
rooms = [record.room_type_id]
|
||||
for room in rooms:
|
||||
# REVIEW -> maybe would be more efficient creating a list
|
||||
# and write all data in 1 operation
|
||||
if record.massive_changes_on == "pricelist":
|
||||
|
||||
dt_from = datetime.datetime.combine(
|
||||
date,
|
||||
datetime.time.min,
|
||||
)
|
||||
dt_to = datetime.datetime.combine(
|
||||
date,
|
||||
datetime.time.max,
|
||||
)
|
||||
|
||||
dt_from = pytz.timezone(tz).localize(dt_from)
|
||||
dt_to = pytz.timezone(tz).localize(dt_to)
|
||||
|
||||
dt_from = dt_from.astimezone(pytz.utc)
|
||||
dt_to = dt_to.astimezone(pytz.utc)
|
||||
|
||||
dt_from = dt_from.replace(tzinfo=None)
|
||||
dt_to = dt_to.replace(tzinfo=None)
|
||||
|
||||
self.env["product.pricelist.item"].create(
|
||||
{
|
||||
"pricelist_id": record.pricelist_id.id,
|
||||
"date_start": dt_from,
|
||||
"date_end": dt_to,
|
||||
"compute_price": "fixed",
|
||||
"applied_on": "1_product",
|
||||
"product_tmpl_id": room.product_id.product_tmpl_id.id,
|
||||
"fixed_price": record.price,
|
||||
"min_quantity": record.min_quantity,
|
||||
}
|
||||
)
|
||||
|
||||
else:
|
||||
self.env["pms.room.type.availability.rule"].create(
|
||||
{
|
||||
"availability_plan_id": record.availability_plan_id.id,
|
||||
"date": date,
|
||||
"room_type_id": room.id,
|
||||
"quota": record.quota,
|
||||
"max_avail": record.max_avail,
|
||||
"min_stay": record.min_stay,
|
||||
"min_stay_arrival": record.min_stay_arrival,
|
||||
"max_stay": record.max_stay,
|
||||
"max_stay_arrival": record.max_stay_arrival,
|
||||
"closed": record.closed,
|
||||
"closed_arrival": record.closed_arrival,
|
||||
"closed_departure": record.closed_departure,
|
||||
}
|
||||
)
|
||||
|
||||
# return {}
|
||||
249
pms/wizards/wizard_massive_changes.xml
Normal file
249
pms/wizards/wizard_massive_changes.xml
Normal file
@@ -0,0 +1,249 @@
|
||||
<?xml version="1.0" ?>
|
||||
<odoo>
|
||||
<record id="massive_changes_wizard" model="ir.ui.view">
|
||||
<field name="name">Availability Wizard</field>
|
||||
<field name="model">pms.massive.changes.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form class="pt-1">
|
||||
<group>
|
||||
<field name="avail_readonly" invisible="1" />
|
||||
<field name="pricelist_readonly" invisible="1" />
|
||||
</group>
|
||||
<div class="row">
|
||||
<div class="col-5 ">
|
||||
<group>
|
||||
<field
|
||||
name="start_date"
|
||||
widget="daterange"
|
||||
options="{'related_end_date': 'end_date'}"
|
||||
/>
|
||||
<field
|
||||
name="end_date"
|
||||
widget="daterange"
|
||||
options="{'related_start_date': 'start_date'}"
|
||||
/>
|
||||
</group>
|
||||
</div>
|
||||
<div class="col-5">
|
||||
<group class="">
|
||||
<field
|
||||
name="massive_changes_on"
|
||||
attrs="{'invisible':['|', ('avail_readonly','=',True), ('pricelist_readonly', '=', True)]}"
|
||||
/>
|
||||
<field
|
||||
name="availability_plan_id"
|
||||
class="mr-5"
|
||||
string="Availability Plan"
|
||||
attrs="{'invisible':['|','|',('massive_changes_on','=','pricelist'), ('avail_readonly','=',True),
|
||||
('pricelist_readonly', '=', True)],
|
||||
'required': [('massive_changes_on','=','availability_plan')]}"
|
||||
/>
|
||||
|
||||
<field
|
||||
name="pricelist_id"
|
||||
string="Pricelist"
|
||||
attrs="{'invisible':['|','|',('massive_changes_on','=','availability_plan'),
|
||||
('pricelist_readonly','=',True), ('pricelist_readonly', '=', True)],
|
||||
'required': [('massive_changes_on','=','pricelist')]}"
|
||||
domain="[('id', 'in', allowed_pricelist_ids)]"
|
||||
/>
|
||||
<field name="room_type_id" default_focus="1" />
|
||||
</group>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<field name="allowed_pricelist_ids" invisible="1" />
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<group>
|
||||
<table class="table table-bordered text-center">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>All days</th>
|
||||
<th>Sunday</th>
|
||||
<th>Monday</th>
|
||||
<th>Tuesday</th>
|
||||
<th>Wednesday</th>
|
||||
<th>Thursday</th>
|
||||
<th>Friday</th>
|
||||
<th>Saturday</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<field
|
||||
name="apply_on_all_week"
|
||||
widget="boolean_toggle"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<field
|
||||
name="apply_on_sunday"
|
||||
widget="boolean_toggle"
|
||||
attrs="{'invisible':[('apply_on_all_week','=',True)]}"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<field
|
||||
name="apply_on_monday"
|
||||
widget="boolean_toggle"
|
||||
attrs="{'invisible':[('apply_on_all_week','=',True)]}"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<field
|
||||
name="apply_on_tuesday"
|
||||
widget="boolean_toggle"
|
||||
attrs="{'invisible':[('apply_on_all_week','=',True)]}"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<field
|
||||
name="apply_on_wednesday"
|
||||
widget="boolean_toggle"
|
||||
attrs="{'invisible':[('apply_on_all_week','=',True)]}"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<field
|
||||
name="apply_on_thursday"
|
||||
widget="boolean_toggle"
|
||||
attrs="{'invisible':[('apply_on_all_week','=',True)]}"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<field
|
||||
name="apply_on_friday"
|
||||
widget="boolean_toggle"
|
||||
attrs="{'invisible':[('apply_on_all_week','=',True)]}"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<field
|
||||
name="apply_on_saturday"
|
||||
widget="boolean_toggle"
|
||||
attrs="{'invisible':[('apply_on_all_week','=',True)]}"
|
||||
/>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</group>
|
||||
</div>
|
||||
</div>
|
||||
<b
|
||||
attrs="{'invisible':['|','|', ('massive_changes_on','=','pricelist'), ('avail_readonly','=',True), ('pricelist_readonly', '=', True)]}"
|
||||
>
|
||||
Rules to apply:
|
||||
</b>
|
||||
|
||||
<div
|
||||
class="row"
|
||||
attrs="{'invisible':[('massive_changes_on','=','pricelist')]}"
|
||||
>
|
||||
|
||||
<div class="col-4">
|
||||
<group>
|
||||
<field string="Min. Stay" name="min_stay" />
|
||||
<field string="Max. Stay" name="max_stay" />
|
||||
</group>
|
||||
</div>
|
||||
|
||||
<div class="col-4">
|
||||
<group>
|
||||
<field string="Min. Stay Arrival" name="min_stay_arrival" />
|
||||
<field string="Max. Stay Arrival" name="max_stay_arrival" />
|
||||
</group>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<group>
|
||||
<field string="Closed" name="closed" />
|
||||
<field string="Closed Arrival" name="closed_arrival" />
|
||||
<field string="Closed departure" name="closed_departure" />
|
||||
</group>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<group>
|
||||
<field string="Quota" name="quota" />
|
||||
</group>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<group>
|
||||
<field string="Max. Avail." name="max_avail" />
|
||||
</group>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="row"
|
||||
attrs="{'invisible':[('massive_changes_on','=','availability_plan')]}"
|
||||
>
|
||||
<div class="col-4">
|
||||
<group>
|
||||
<field name="price" />
|
||||
<field name="min_quantity" />
|
||||
</group>
|
||||
</div>
|
||||
</div>
|
||||
<b attrs="{'invisible':[('massive_changes_on','=','pricelist')]}">
|
||||
<span style="text-decoration:underline;">
|
||||
<field name="num_rules_to_overwrite" />
|
||||
</span>
|
||||
availability rules
|
||||
<u>will be overwritten:</u>
|
||||
</b>
|
||||
|
||||
<b
|
||||
attrs="{'invisible':[('massive_changes_on','=','availability_plan')]}"
|
||||
>
|
||||
<span style="text-decoration:underline;">
|
||||
<field name="num_pricelist_items_to_overwrite" />
|
||||
</span>
|
||||
pricelist items
|
||||
<u>will be overwritten:</u>
|
||||
</b>
|
||||
|
||||
|
||||
<!-- REVIEW: a calendar view could be more understandable -->
|
||||
<field
|
||||
name="rules_to_overwrite"
|
||||
attrs="{'invisible':[('massive_changes_on','=','pricelist')]}"
|
||||
/>
|
||||
<field
|
||||
name="pricelist_items_to_overwrite"
|
||||
attrs="{'invisible':[('massive_changes_on','=','availability_plan')]}"
|
||||
/>
|
||||
<footer>
|
||||
<button
|
||||
name="apply_massive_changes"
|
||||
string="Apply changes"
|
||||
type="object"
|
||||
class="btn-primary"
|
||||
/>
|
||||
or
|
||||
<button
|
||||
string="Cancel"
|
||||
class="btn-default border"
|
||||
special="cancel"
|
||||
/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="action_wizard_massive_changes" model="ir.actions.act_window">
|
||||
<field name="name">Massive changes on Pricelist & Availability Plans</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">pms.massive.changes.wizard</field>
|
||||
<field name="view_id" ref="massive_changes_wizard" />
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
<menuitem
|
||||
name="Massive Changes"
|
||||
id="menu_pms_room_massive_changes_wizard"
|
||||
action="action_wizard_massive_changes"
|
||||
sequence="25"
|
||||
parent="pms.configuration_others"
|
||||
/>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user