[MIG] customer_applications to 18.0
This commit is contained in:
parent
e1a64337ff
commit
d69c2425ee
16 changed files with 560 additions and 0 deletions
1
customer_applications/__init__.py
Normal file
1
customer_applications/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import models
|
||||
40
customer_applications/__manifest__.py
Normal file
40
customer_applications/__manifest__.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
#
|
||||
# Bemade Inc.
|
||||
#
|
||||
# Copyright (C) 2023-June Bemade Inc. (<https://www.bemade.org>).
|
||||
# Author: Marc Durepos (Contact : marc@bemade.org)
|
||||
#
|
||||
# This program is under the terms of the GNU Lesser General Public License,
|
||||
# version 3.
|
||||
#
|
||||
# For full license details, see https://www.gnu.org/licenses/lgpl-3.0.en.html.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
# DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
{
|
||||
"name": "Customer Applications",
|
||||
"version": "18.0.1.0.0",
|
||||
"summary": "Adds the notion of applications to partners.",
|
||||
"category": "Contacts",
|
||||
"author": "Bemade Inc.",
|
||||
"website": "http://www.bemade.org",
|
||||
"license": "LGPL-3",
|
||||
"depends": ["contacts", "incrementing_sequence_mixin"],
|
||||
"data": [
|
||||
"security/groups.xml",
|
||||
"security/ir.model.access.csv",
|
||||
"data/menus_actions.xml",
|
||||
"views/application_type_views.xml",
|
||||
"views/res_partner_views.xml",
|
||||
"views/application_views.xml",
|
||||
],
|
||||
"assets": {},
|
||||
"installable": True,
|
||||
"auto_install": False,
|
||||
}
|
||||
43
customer_applications/data/menus_actions.xml
Normal file
43
customer_applications/data/menus_actions.xml
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="view_application_types" model="ir.actions.act_window">
|
||||
<field name="name">Application Types</field>
|
||||
<field name="res_model">partner.application.type</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No application types found. Create one?
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="partner_related_applications_action" model="ir.actions.act_window">
|
||||
<field name="name">Partner Applications</field>
|
||||
<field name="res_model">partner.application</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="domain">[('partner_id', '=', context.get('partner_id'))]</field>
|
||||
<field name="context">{'default_partner_id': context.get('partner_id')}</field>
|
||||
</record>
|
||||
|
||||
<record id="action_view_partner_applications_by_type" model="ir.actions.act_window">
|
||||
<field name="name">Partner Applications by Type</field>
|
||||
<field name="res_model">partner.application</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="domain">[
|
||||
('application_type_id', '=', context.get('application_type_id'))
|
||||
]
|
||||
</field>
|
||||
<field name="context">{
|
||||
'default_application_type_id': context.get('application_type_id')
|
||||
}
|
||||
</field>
|
||||
</record>
|
||||
<menuitem id="application_types"
|
||||
parent="contacts.res_partner_menu_config"
|
||||
groups="group_applications_admin"
|
||||
action="view_application_types"
|
||||
/>
|
||||
<record id="contacts.res_partner_menu_config" model="ir.ui.menu">
|
||||
<field name="sequence" eval="99"/>
|
||||
</record>
|
||||
</odoo>
|
||||
5
customer_applications/models/__init__.py
Normal file
5
customer_applications/models/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from . import application
|
||||
from . import application_specification
|
||||
from . import application_type
|
||||
from . import res_partner
|
||||
from . import application_specification_key
|
||||
36
customer_applications/models/application.py
Normal file
36
customer_applications/models/application.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
from odoo import models, fields, Command
|
||||
|
||||
|
||||
class Application(models.Model):
|
||||
_name = "partner.application"
|
||||
_description = "Partner Application"
|
||||
_inherit = ["mail.thread", "mail.activity.mixin"]
|
||||
|
||||
name = fields.Char(tracking=1)
|
||||
description = fields.Text(tracking=2)
|
||||
partner_id = fields.Many2one(
|
||||
comodel_name="res.partner",
|
||||
required=True,
|
||||
tracking=3,
|
||||
copy=False,
|
||||
)
|
||||
application_type_id = fields.Many2one(
|
||||
comodel_name="partner.application.type",
|
||||
required=True,
|
||||
tracking=4,
|
||||
ondelete="restrict",
|
||||
)
|
||||
specification_ids = fields.One2many(
|
||||
comodel_name="partner.application.specification",
|
||||
inverse_name="application_id",
|
||||
tracking=5,
|
||||
)
|
||||
|
||||
def copy(self, default=None):
|
||||
self.ensure_one() # This logic won't work for batches, and it doesn't need to
|
||||
default = default or {}
|
||||
if "specification_ids" not in default:
|
||||
default["specification_ids"] = [
|
||||
Command.create(line.copy_data()[0]) for line in self.specification_ids
|
||||
]
|
||||
return super().copy(default)
|
||||
42
customer_applications/models/application_specification.py
Normal file
42
customer_applications/models/application_specification.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class PartnerApplicationSpecification(models.Model):
|
||||
_name = "partner.application.specification"
|
||||
_description = "Partner Application Specification"
|
||||
_inherit = ["mail.thread", "mail.activity.mixin", "incrementing.sequence.mixin"]
|
||||
_sequence_group = "application_id"
|
||||
|
||||
key_id = fields.Many2one(
|
||||
comodel_name="partner.application.specification.key",
|
||||
tracking=1,
|
||||
ondelete="restrict",
|
||||
domain="[('id', 'in', allowed_specification_keys)]",
|
||||
string="Specification Name",
|
||||
required=True,
|
||||
)
|
||||
name = fields.Char(
|
||||
related="key_id.name",
|
||||
)
|
||||
value = fields.Text(
|
||||
tracking=2,
|
||||
)
|
||||
application_id = fields.Many2one(
|
||||
comodel_name="partner.application",
|
||||
tracking=1,
|
||||
ondelete="cascade",
|
||||
)
|
||||
allowed_specification_keys = fields.Many2many(
|
||||
related="application_id.application_type_id.allowed_specification_keys",
|
||||
)
|
||||
|
||||
@api.constrains("key_id")
|
||||
def _constrain_key_id(self):
|
||||
for rec in self:
|
||||
if rec.key_id not in rec.allowed_specification_keys:
|
||||
raise ValidationError(
|
||||
_(
|
||||
f"Key '{rec.key_id.name}' is not allowed for this application type."
|
||||
)
|
||||
)
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class ApplicationSpecificationKey(models.Model):
|
||||
_name = "partner.application.specification.key"
|
||||
_description = "Application Specification Key"
|
||||
|
||||
name = fields.Char(required=True, index="trigram")
|
||||
_sql_constraints = [
|
||||
("name_uniq", "unique (name)", "Specification key name must be unique."),
|
||||
]
|
||||
54
customer_applications/models/application_type.py
Normal file
54
customer_applications/models/application_type.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class PartnerApplicationType(models.Model):
|
||||
_name = "partner.application.type"
|
||||
_description = "Partner Application Type"
|
||||
|
||||
_inherit = ["mail.thread", "mail.activity.mixin"]
|
||||
|
||||
color = fields.Integer()
|
||||
name = fields.Char(
|
||||
required=True,
|
||||
tracking=1,
|
||||
)
|
||||
description = fields.Text(tracking=2)
|
||||
application_ids = fields.One2many(
|
||||
comodel_name="partner.application",
|
||||
inverse_name="application_type_id",
|
||||
tracking=3,
|
||||
)
|
||||
|
||||
applications_count = fields.Integer(
|
||||
string="Applications Count",
|
||||
compute="_compute_applications_count",
|
||||
)
|
||||
|
||||
partner_ids = fields.One2many(
|
||||
comodel_name="res.partner",
|
||||
compute="_compute_partner_ids",
|
||||
search="_search_partner_ids",
|
||||
string="Partners",
|
||||
readonly=True,
|
||||
)
|
||||
allowed_specification_keys = fields.Many2many(
|
||||
comodel_name="partner.application.specification.key",
|
||||
relation="application_specification_key_application_type_rel",
|
||||
column1="application_type_id",
|
||||
column2="application_specification_key_id",
|
||||
)
|
||||
|
||||
@api.depends("application_ids", "application_ids.partner_id")
|
||||
def _compute_partner_ids(self):
|
||||
for application_type in self:
|
||||
application_type.partner_ids = application_type.application_ids.mapped(
|
||||
"partner_id"
|
||||
)
|
||||
|
||||
def _search_partner_ids(self, operator, value):
|
||||
return [("application_ids.partner_id", operator, value)]
|
||||
|
||||
@api.depends("application_ids")
|
||||
def _compute_applications_count(self):
|
||||
for record in self:
|
||||
record.applications_count = len(record.application_ids)
|
||||
42
customer_applications/models/res_partner.py
Normal file
42
customer_applications/models/res_partner.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_inherit = "res.partner"
|
||||
|
||||
application_ids = fields.One2many(
|
||||
"partner.application",
|
||||
"partner_id",
|
||||
string="Applications",
|
||||
)
|
||||
applications_count = fields.Integer(
|
||||
string="Applications Count",
|
||||
compute="_compute_applications_count",
|
||||
)
|
||||
application_type_ids = fields.One2many(
|
||||
comodel_name="partner.application.type",
|
||||
compute="_compute_application_type_ids",
|
||||
string="Application Types",
|
||||
readonly=True,
|
||||
search="_search_application_type_ids",
|
||||
)
|
||||
|
||||
@api.depends("application_ids.application_type_id")
|
||||
def _compute_application_type_ids(self):
|
||||
for partner in self:
|
||||
partner.application_type_ids = partner.application_ids.mapped(
|
||||
"application_type_id"
|
||||
)
|
||||
|
||||
def _search_application_type_ids(self, operator, value):
|
||||
return [("application_ids.application_type_id", operator, value)]
|
||||
|
||||
@api.depends("application_ids")
|
||||
def _compute_applications_count(self):
|
||||
for partner in self:
|
||||
partner.applications_count = len(partner.application_ids)
|
||||
|
||||
@api.depends("application_ids")
|
||||
def _compute_applications_count(self):
|
||||
for rec in self:
|
||||
rec.applications_count = len(rec.application_ids)
|
||||
14
customer_applications/security/groups.xml
Normal file
14
customer_applications/security/groups.xml
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="group_applications_user" model="res.groups">
|
||||
<field name="name">Applications User</field>
|
||||
</record>
|
||||
<record id="group_applications_admin" model="res.groups">
|
||||
<field name="name">Applications Admin</field>
|
||||
</record>
|
||||
<record id="base.group_user" model="res.groups">
|
||||
<field name="implied_ids" eval="[(4, ref('group_applications_user'))]"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
7
customer_applications/security/ir.model.access.csv
Normal file
7
customer_applications/security/ir.model.access.csv
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_partner_application_user,access.partner.application.user,model_partner_application,group_applications_user,1,1,1,1
|
||||
access_partner_application_type_user,access.partner.application.type.user,model_partner_application_type,group_applications_user,1,0,0,0
|
||||
access_partner_application_type_manager,access.partner.application.type.manager,model_partner_application_type,group_applications_admin,1,1,1,1
|
||||
access_partner_application_specification_user,access.partner.application.specification.user,model_partner_application_specification,group_applications_user,1,1,1,1
|
||||
access_partner_application_specification_key_user,access.partner.application.specification.key.user,model_partner_application_specification_key,group_applications_user,1,0,0,0
|
||||
access_partner_application_specification_key_admin,access.partner.application.specification.key.admin,model_partner_application_specification_key,group_applications_admin,1,1,1,1
|
||||
|
1
customer_applications/tests/__init__.py
Normal file
1
customer_applications/tests/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import test_application
|
||||
59
customer_applications/tests/test_application.py
Normal file
59
customer_applications/tests/test_application.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
from odoo.tests import TransactionCase
|
||||
|
||||
|
||||
class TestApplication(TransactionCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.partner_1, cls.partner_2 = cls.env["res.partner"].create(
|
||||
[
|
||||
{
|
||||
"name": "Test Partner",
|
||||
},
|
||||
{
|
||||
"name": "Test Partner 2",
|
||||
},
|
||||
]
|
||||
)
|
||||
cls.application_type = cls.env["partner.application.type"].create(
|
||||
{
|
||||
"name": "application type",
|
||||
}
|
||||
)
|
||||
|
||||
def test_copy_correctly_creates_specification_lines(self):
|
||||
application = self.env["partner.application"].create(
|
||||
{
|
||||
"partner_id": self.partner_1.id,
|
||||
"application_type_id": self.application_type.id,
|
||||
}
|
||||
)
|
||||
specifications = self.env["partner.application.specification"].create(
|
||||
[
|
||||
{
|
||||
"name": "Spec 1",
|
||||
"value": "Spec 1 value",
|
||||
"application_id": application.id,
|
||||
},
|
||||
{
|
||||
"name": "Spec 2",
|
||||
"value": "Spec 2 value",
|
||||
"application_id": application.id,
|
||||
},
|
||||
]
|
||||
)
|
||||
application_copy = application.copy(
|
||||
default={
|
||||
"partner_id": self.partner_2.id,
|
||||
}
|
||||
)
|
||||
self.assertEqual(len(application_copy.specification_ids), 2)
|
||||
self.assertEqual(len(application.specification_ids), 2)
|
||||
self.assertNotEqual(
|
||||
application.specification_ids, application_copy.specification_ids
|
||||
)
|
||||
|
||||
# TODO: move this to a test in the mixin module once we figure out how
|
||||
# to dynamically create and load models
|
||||
self.assertEqual(specifications[0].sequence, 1)
|
||||
self.assertEqual(specifications[1].sequence, 2)
|
||||
74
customer_applications/views/application_type_views.xml
Normal file
74
customer_applications/views/application_type_views.xml
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="view_partner_application_type_list" model="ir.ui.view">
|
||||
<field name="name">partner.application.type.list</field>
|
||||
<field name="model">partner.application.type</field>
|
||||
<field name="arch" type="xml">
|
||||
<list multi_edit="True">
|
||||
<field name="name"/>
|
||||
<field name="description"/>
|
||||
<field name="color" widget="color_picker"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_partner_application_type_form" model="ir.ui.view">
|
||||
<field name="name">partner.application.type.form</field>
|
||||
<field name="model">partner.application.type</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<header/>
|
||||
<sheet>
|
||||
<div name="button_box" class="oe_button_box">
|
||||
<button name="%(action_view_partner_applications_by_type)d"
|
||||
type="action"
|
||||
class="oe_stat_button"
|
||||
context="{'application_type_id': id}"
|
||||
icon="fa-gears"
|
||||
>
|
||||
<field name="applications_count" string="Applications"
|
||||
widget="statinfo"/>
|
||||
</button>
|
||||
</div>
|
||||
<div name="title" class="oe_title">
|
||||
<div class="oe_title">
|
||||
<div class="oe_edit_only">
|
||||
Application Type
|
||||
</div>
|
||||
<h1>
|
||||
<field name="name"/>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<group>
|
||||
<field name="description"/>
|
||||
<field name="color" widget="color_picker"/>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Allowed Specifications"
|
||||
groups="customer_applications.group_applications_admin">
|
||||
<field name="allowed_specification_keys" widget="many2many_tags"/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids"/>
|
||||
<field name="message_ids"/>
|
||||
<field name="activity_ids"/>
|
||||
</div>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_partner_application_type_search" model="ir.ui.view">
|
||||
<field name="name">partner.application.type.search</field>
|
||||
<field name="model">partner.application.type</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="name"/>
|
||||
<field name="description"/>
|
||||
<field name="partner_ids"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
86
customer_applications/views/application_views.xml
Normal file
86
customer_applications/views/application_views.xml
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="view_partner_application_form" model="ir.ui.view">
|
||||
<field name="name">partner.application.form</field>
|
||||
<field name="model">partner.application</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Partner Application">
|
||||
<header/>
|
||||
<sheet>
|
||||
<div name="button_box" class="oe_button_box"/>
|
||||
<div name="title" class="oe_title">
|
||||
<div class="oe_title">
|
||||
<div class="oe_edit_only">
|
||||
Application
|
||||
</div>
|
||||
<h1>
|
||||
<field name="name"/>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="partner_id"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="application_type_id"/>
|
||||
<field name="description"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Specifications">
|
||||
<field name="specification_ids">
|
||||
<list editable="bottom">
|
||||
<field name="allowed_specification_keys" column_invisible="1"/>
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="key_id" />
|
||||
<field name="value"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_ids"/>
|
||||
<field name="message_follower_ids"/>
|
||||
<field name="activity_ids"/>
|
||||
</div>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_partner_application_list" model="ir.ui.view">
|
||||
<field name="name">partner.application.list</field>
|
||||
<field name="model">partner.application</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Partner Applications">
|
||||
<field name="name"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="application_type_id"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_partner_application_search" model="ir.ui.view">
|
||||
<field name="name">partner.application.search</field>
|
||||
<field name="model">partner.application</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Partner Applications">
|
||||
<field name="name"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="application_type_id"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="partner_application_action" model="ir.actions.act_window">
|
||||
<field name="name">Partner Applications</field>
|
||||
<field name="res_model">partner.application</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="view_id" ref="view_partner_application_list"/>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_partner_application" name="Applications"
|
||||
parent="contacts.menu_contacts"
|
||||
action="partner_application_action"/>
|
||||
</odoo>
|
||||
45
customer_applications/views/res_partner_views.xml
Normal file
45
customer_applications/views/res_partner_views.xml
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="view_partner_form" model="ir.ui.view">
|
||||
<field name="name">view.partner.form</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<div name="button_box" position="inside">
|
||||
<button name="%(partner_related_applications_action)d"
|
||||
type="action"
|
||||
class="oe_stat_button"
|
||||
icon="fa-cogs"
|
||||
context="{'partner_id': id}"
|
||||
>
|
||||
<field string="Applications" name="applications_count"
|
||||
widget="statinfo"/>
|
||||
</button>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_partner_list" model="ir.ui.view">
|
||||
<field name="name">view.partner.list</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='complete_name']" position="after">
|
||||
<field name="application_type_ids" widget="many2many_tags"
|
||||
optional="show"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_partner_search" model="ir.ui.view">
|
||||
<field name="name">view.partner.search</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_res_partner_filter"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="user_id" position="after">
|
||||
<field name="application_type_ids" string="Application Type"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
Loading…
Reference in a new issue