-
Notifications
You must be signed in to change notification settings - Fork 3.1k
vibad - technical trainning #1239
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 19.0
Are you sure you want to change the base?
Changes from all commits
11a9e65
e8c4542
ebe26b3
760eba1
86dbba3
842b9fb
d3a2e67
0eeb0b0
45320c8
b2e44d5
7ab1515
c4f1032
0895297
e5985e4
bce8978
bf2cafe
867cbfe
9061548
8d99e23
5dd56af
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| from . import models |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| { | ||
| 'name': 'estate', | ||
| 'depends': [ | ||
| 'base' | ||
| ], | ||
| 'installable': True, | ||
| 'application': True, | ||
| 'author': 'vibad', | ||
| 'license': 'LGPL-3', | ||
| 'version': '1.0', | ||
| 'data': [ | ||
| 'security/ir.model.access.csv', | ||
| 'view/estate_property_offer_views.xml', | ||
| 'view/estate_property_views.xml', | ||
| 'view/estate_property_type_views.xml', | ||
| 'view/estate_property_tag_views.xml', | ||
| 'view/estate_inherit_view.xml', | ||
| 'view/estate_action.xml', | ||
| ], | ||
|
|
||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| from . import estate_property | ||
| from . import estate_property_type | ||
| from . import estate_property_tag | ||
| from . import estate_property_offer | ||
| from . import inherited_model |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,115 @@ | ||||||
| from odoo import api, fields, models | ||||||
| from odoo.exceptions import UserError, ValidationError | ||||||
|
|
||||||
|
|
||||||
| class Estate_property(models.Model): | ||||||
|
nausicaa73 marked this conversation as resolved.
|
||||||
| _name = "estate.property" | ||||||
| _description = "APP super mega trop bien" | ||||||
| _order = "id desc" | ||||||
|
|
||||||
| name = fields.Char(required=True) | ||||||
|
nausicaa73 marked this conversation as resolved.
|
||||||
| description = fields.Text() | ||||||
| postcode = fields.Char() | ||||||
| date_availability = fields.Date(copy=False, default=lambda self: fields.Date.add(fields.Date.today(), months=3)) | ||||||
| expected_price = fields.Float(required=True) | ||||||
| selling_price = fields.Float(readonly=True, copy=False) | ||||||
| bedrooms = fields.Integer(default=2) | ||||||
| living_area = fields.Integer() | ||||||
| facades = fields.Integer() | ||||||
| garage = fields.Boolean() | ||||||
| garden = fields.Boolean() | ||||||
| garden_area = fields.Integer() | ||||||
| garden_orientation = fields.Selection( | ||||||
| string="Orientation", | ||||||
| selection=[("north", "North"), ("south", "South"), ("east", "East"), ("west", "West")], | ||||||
| help="The garden orientation", | ||||||
| ) | ||||||
| active = fields.Boolean(default=True) | ||||||
| state = fields.Selection( | ||||||
| string="Status", | ||||||
| selection=[("new", "New"), ("offer_received", "Offer Received"), ("offer_accepted", "Offer Accepted"), ("sold", "Sold"), ("cancelled", "Cancelled")], | ||||||
| required=True, | ||||||
| copy=False, | ||||||
| default="new", | ||||||
| compute="_compute_state", | ||||||
| store=True, | ||||||
| ) | ||||||
|
nausicaa73 marked this conversation as resolved.
|
||||||
| property_type_id = fields.Many2one("estate.property.type", string="Property Type") | ||||||
| salesperson_id = fields.Many2one("res.users", string="Salesperson", default=lambda self: self.env.user) | ||||||
| buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False) | ||||||
| tag_ids = fields.Many2many("estate.property.tag", string="Tags") | ||||||
| offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers") | ||||||
| total_area = fields.Integer(compute="_compute_area") | ||||||
| best_price = fields.Float(compute="_compute_best_price") | ||||||
|
|
||||||
| _check_expected_price = models.Constraint( | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Order of the elements of the file is wrong, constraints should come after computes (see doc) |
||||||
| "CHECK(expected_price > 0)", | ||||||
| message="The expected price must be strictly positive", | ||||||
| ) | ||||||
|
|
||||||
| _check_selling_price = models.Constraint( | ||||||
| "CHECK(selling_price >= 0)", | ||||||
| message="The selling price cannot be negative", | ||||||
| ) | ||||||
|
|
||||||
| @api.depends("living_area", "garden_area") | ||||||
| def _compute_area(self): | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need to shorten the name, especially since there are multiple variables named
Suggested change
|
||||||
| for record in self: | ||||||
| record.total_area = record.living_area + record.garden_area | ||||||
|
|
||||||
| @api.depends("offer_ids.price") | ||||||
| def _compute_best_price(self): | ||||||
| for record in self: | ||||||
| if record.offer_ids: | ||||||
| record.best_price = max(record.offer_ids.mapped("price")) | ||||||
| else: | ||||||
| record.best_price = 0 | ||||||
|
|
||||||
| @api.onchange("garden") | ||||||
| def _onchange_garden(self): | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tricky (?) question time! At first glance, this method could also be an Answer
It doesn't make much sense to set arbitrary default values when programmatically updating records, so |
||||||
| for record in self: | ||||||
| if record.garden: | ||||||
| record.garden_area = 10 | ||||||
| record.garden_orientation = "north" | ||||||
| else: | ||||||
| record.garden_area = 0 | ||||||
| record.garden_orientation = False | ||||||
|
|
||||||
| @api.depends("offer_ids", "offer_ids.state") | ||||||
| def _compute_state(self): | ||||||
| for record in self: | ||||||
| if record.state in ["sold", "cancelled"]: | ||||||
| return | ||||||
| if record.offer_ids: | ||||||
| for offer in record.offer_ids: | ||||||
| if offer.state == "accepted": | ||||||
| record.state = "offer_accepted" | ||||||
| break | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We want to stop after updating, so
Suggested change
|
||||||
| else: | ||||||
| record.state = "new" | ||||||
|
|
||||||
| def action_sold(self): | ||||||
| for record in self: | ||||||
| if record.state != "cancelled" and record.state != "sold": | ||||||
| record.state = "sold" | ||||||
| else: | ||||||
| raise UserError("A property that is cancelled or already sold cannot be sold.") | ||||||
|
|
||||||
| def action_cancel(self): | ||||||
| for record in self: | ||||||
| if record.state != "sold" and record.state != "cancelled": | ||||||
| record.state = "cancelled" | ||||||
| else: | ||||||
| raise UserError("A property that is sold or already cancelled cannot be cancelled.") | ||||||
|
|
||||||
| @api.constrains("selling_price", "expected_price") | ||||||
| def _check_enough_selling_price(self): | ||||||
| for record in self: | ||||||
| if record.selling_price and record.selling_price < record.expected_price * 0.9: | ||||||
| raise ValidationError("The selling price cannot be less than 90% of the expected price.") | ||||||
|
|
||||||
| @api.ondelete(at_uninstall=False) | ||||||
| def _ondelete_cancel_new(self): | ||||||
| for record in self: | ||||||
| if record.state not in ["new", "cancelled"]: | ||||||
| raise UserError("You can only delete offers that are new or cancelled.") | ||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,78 @@ | ||||||||||||||||||||||||||||
| from odoo import api, fields, models | ||||||||||||||||||||||||||||
| from odoo.exceptions import UserError | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| class Estate_property_offer(models.Model): | ||||||||||||||||||||||||||||
| _name = "estate.property.offer" | ||||||||||||||||||||||||||||
| _description = "Offer for estate properties" | ||||||||||||||||||||||||||||
| _order = "price desc" | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| price = fields.Float(required=True) | ||||||||||||||||||||||||||||
| partner_id = fields.Many2one("res.partner", string="Partner", required=True) | ||||||||||||||||||||||||||||
| property_id = fields.Many2one("estate.property", string="Property", required=True) | ||||||||||||||||||||||||||||
| state = fields.Selection([ | ||||||||||||||||||||||||||||
| ("new", "New"), | ||||||||||||||||||||||||||||
| ("accepted", "Accepted"), | ||||||||||||||||||||||||||||
| ("refused", "Refused"), | ||||||||||||||||||||||||||||
| ], default="new", string="State", copy=False) | ||||||||||||||||||||||||||||
| validaty = fields.Integer(string="Offer Validity (days)", default=7) | ||||||||||||||||||||||||||||
| date_deadline = fields.Date(string="Offer Deadline", compute="_compute_date_deadline", inverse="_inverse_date_deadline") | ||||||||||||||||||||||||||||
| property_type_id = fields.Many2one(related="property_id.property_type_id", store=True) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| _check_price = models.Constraint( | ||||||||||||||||||||||||||||
| "CHECK(price > 0)", | ||||||||||||||||||||||||||||
| message="The price must be strictly positive", | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| @api.depends("validaty", "create_date") | ||||||||||||||||||||||||||||
| def _compute_date_deadline(self): | ||||||||||||||||||||||||||||
| for record in self: | ||||||||||||||||||||||||||||
| date = record.create_date | ||||||||||||||||||||||||||||
| if not date: | ||||||||||||||||||||||||||||
| date = fields.Date.today() | ||||||||||||||||||||||||||||
| if record.validaty: | ||||||||||||||||||||||||||||
| record.date_deadline = fields.Date.add(date, days=record.validaty) | ||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||
| record.date_deadline = False | ||||||||||||||||||||||||||||
|
Comment on lines
+28
to
+36
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To get the actual date, you need to call Also, you provide a default value for
Suggested change
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| def _inverse_date_deadline(self): | ||||||||||||||||||||||||||||
| for record in self: | ||||||||||||||||||||||||||||
| if record.date_deadline and record.create_date: | ||||||||||||||||||||||||||||
| create_date = fields.Date.to_date(record.create_date) | ||||||||||||||||||||||||||||
| record.validaty = (record.date_deadline - create_date).days | ||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||
| record.validaty = 0 | ||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't you default to 7? |
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| def accept_offer(self): | ||||||||||||||||||||||||||||
| for record in self: | ||||||||||||||||||||||||||||
| if record.state == "accepted" or record.state == "refused": | ||||||||||||||||||||||||||||
| raise UserError("This offer has already been accepted or refused.") | ||||||||||||||||||||||||||||
| for offer in record.property_id.offer_ids: | ||||||||||||||||||||||||||||
| if offer.state == "accepted": | ||||||||||||||||||||||||||||
|
Comment on lines
+50
to
+51
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is fine, but here's a one liner for comparison
Suggested change
|
||||||||||||||||||||||||||||
| raise UserError("Another offer has already been accepted for this property.") | ||||||||||||||||||||||||||||
| if record.property_id.garden_orientation == "south" and record.price <= record.property_id.expected_price: | ||||||||||||||||||||||||||||
| raise UserError("The price must be more than the expected price for properties with a south-facing garden.") | ||||||||||||||||||||||||||||
| record.state = "accepted" | ||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When an action modifies values on a record, it usually explicitly calls a
Suggested change
|
||||||||||||||||||||||||||||
| record.property_id.selling_price = record.price | ||||||||||||||||||||||||||||
| record.property_id.buyer_id = record.partner_id | ||||||||||||||||||||||||||||
| return True | ||||||||||||||||||||||||||||
|
Comment on lines
+56
to
+58
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can
Suggested change
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| def refuse_offer(self): | ||||||||||||||||||||||||||||
| for record in self: | ||||||||||||||||||||||||||||
| if record.state != "new": | ||||||||||||||||||||||||||||
| raise UserError("This offer has already been accepted or refused.") | ||||||||||||||||||||||||||||
| record.state = "refused" | ||||||||||||||||||||||||||||
| return True | ||||||||||||||||||||||||||||
|
Comment on lines
+64
to
+65
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same remark |
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| @api.model_create_multi | ||||||||||||||||||||||||||||
| def create(self, vals_list): | ||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same remark for order of methods |
||||||||||||||||||||||||||||
| max_price_list = 0 | ||||||||||||||||||||||||||||
| for vals in vals_list: | ||||||||||||||||||||||||||||
| if self.env["estate.property"].browse(vals["property_id"]).state == "new": | ||||||||||||||||||||||||||||
| self.env["estate.property"].browse(vals["property_id"]).state = "offer_received" | ||||||||||||||||||||||||||||
| max_price = max(self.env["estate.property.offer"].search([("property_id", "=", vals["property_id"])]).mapped("price") or [0]) | ||||||||||||||||||||||||||||
| max_price_list = max(max_price_list, max_price) | ||||||||||||||||||||||||||||
| if vals["price"] <= max_price: | ||||||||||||||||||||||||||||
| raise UserError("The price must be higher than the current highest offer.") # Error for one offer blocks all offers in the list | ||||||||||||||||||||||||||||
| max_price_list = max(max_price_list, vals["price"]) | ||||||||||||||||||||||||||||
| return super().create(vals_list) | ||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,15 @@ | ||||||
| from odoo import fields, models | ||||||
|
|
||||||
|
|
||||||
| class Estate_property_tag(models.Model): | ||||||
| _name = "estate.property.tag" | ||||||
| _description = "tag super mega trop bien" | ||||||
| _order = "name" | ||||||
|
|
||||||
| name = fields.Char(required=True) | ||||||
| color = fields.Integer() | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. American english is ugly, too bad it's the default most of the time 😢
Suggested change
(don't apply this, it's just me ranting)
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🇺🇸🇺🇸🇺🇸 |
||||||
|
|
||||||
| _check_name = models.Constraint( | ||||||
| "UNIQUE(name)", | ||||||
| message="The name of the tag must be unique", | ||||||
| ) | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| from odoo import fields, models | ||
|
|
||
|
|
||
| class Estate_property_type(models.Model): | ||
| _name = "estate.property.type" | ||
| _description = "APP super mega trop bien" | ||
| _order = "sequence, name" | ||
| _check_name = models.Constraint( | ||
|
nausicaa73 marked this conversation as resolved.
|
||
| "UNIQUE(name)", | ||
| message="The name of the property type must be unique", | ||
| ) | ||
|
|
||
| name = fields.Char(required=True) | ||
| sequence = fields.Integer() | ||
| offer_ids = fields.One2many("estate.property.offer", "property_type_id", string="Offers") | ||
| offer_count = fields.Integer(compute="_compute_offer_count") | ||
| property_ids = fields.One2many("estate.property", "property_type_id", string="Properties") | ||
|
|
||
| def _compute_offer_count(self): | ||
| for record in self: | ||
| record.offer_count = len(record.offer_ids) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| from odoo import fields, models | ||
|
|
||
|
|
||
| class Estate_users_model(models.Model): | ||
| _inherit = "res.users" | ||
|
|
||
| property_ids = fields.One2many("estate.property", "salesperson_id", string=" Estate properties") |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink | ||
| acces_estate_property,estate_property,model_estate_property,base.group_user,1,1,1,1 | ||
| acces_estate_property_type,estate_property_type,model_estate_property_type,base.group_user,1,1,1,0 | ||
| acces_estate_property_tag,estate_property_tag,model_estate_property_tag,base.group_user,1,1,1,0 | ||
| acces_estate_property_offer,estate_property_offer,model_estate_property_offer,base.group_user,1,1,1,1 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| from . import test_estate_property |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| from odoo.exceptions import ValidationError | ||
| from odoo.tests import TransactionCase | ||
| from odoo import Command | ||
|
|
||
|
|
||
| class TestEstateProperty(TransactionCase): | ||
|
|
||
| @classmethod | ||
| def setUpClass(cls): | ||
| super().setUpClass() | ||
| cls.estate = cls.env['estate.property'].create({ | ||
| 'name': 'Super test estate', | ||
| 'expected_price': 100000.0, | ||
| 'state': 'new', | ||
| }) | ||
| cls.test_partner = cls.env['res.partner'].create({ | ||
| 'name': 'Maman ours', | ||
| }) | ||
|
|
||
| def test_estate_best_price(self): | ||
| ''' | ||
| Ensure best price is correctly updated when an offer is received. | ||
| ''' | ||
| self.assertEqual(self.estate.best_price, 0.0) | ||
| self.estate.offer_ids = [Command.create({ | ||
| 'price': 125000.0, | ||
| 'partner_id': self.test_partner.id, | ||
| })] | ||
| self.assertEqual(self.estate.best_price, 125000.0) | ||
|
|
||
| def test_accept_offer_south_facing_garden(self): | ||
| ''' | ||
| Ensure offers for estates with south-facing gardens can only be accepted if above expected | ||
| price. | ||
| ''' | ||
| self.estate.expected_price = 500000 | ||
| self.estate.offer_ids = [Command.create({ | ||
| 'price': 475000.0, | ||
| 'partner_id': self.test_partner.id, | ||
| })] | ||
| with self.assertRaises(ValidationError): | ||
| self.estate.offer_ids.accept_offer() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| <?xml version="1.0"?> | ||
| <odoo> | ||
| <menuitem id="estate_root" name="Real Estate"> | ||
| <menuitem id="advertisements_level_menu" name="Advertisements"> | ||
| <menuitem id="estate_property_menu_action" action="estate_property_action"/> | ||
|
nausicaa73 marked this conversation as resolved.
|
||
| </menuitem> | ||
| <menuitem id="property_types_level_menu" name="Settings"> | ||
| <menuitem id="estate_property_type_menu_action" action="estate_property_type_action"/> | ||
| <menuitem id="estate_property_tag_menu_action" action="estate_property_tag_action"/> | ||
| </menuitem> | ||
| </menuitem> | ||
| </odoo> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| <?xml version="1.0"?> | ||
| <odoo> | ||
| <record id="estate_inherit_view" model="ir.ui.view"> | ||
| <field name="name">estate.inherit.view</field> | ||
| <field name="model">res.users</field> | ||
| <field name="inherit_id" ref="base.view_users_form"/> | ||
| <field name="arch" type="xml"> | ||
| <xpath expr="//page[@name='preferences']" position="after"> | ||
| <page string="Real Estate" name="real_estate"> | ||
| <field name="property_ids" options="{'no_create': True, 'no_open': True}"/> | ||
| </page> | ||
| </xpath> | ||
| </field> | ||
| </record> | ||
| </odoo> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| <?xml version="1.0"?> | ||
| <odoo> | ||
| <record id="estate_property_offer_action" model="ir.actions.act_window"> | ||
| <field name="name">Offers</field> | ||
| <field name="res_model">estate.property.offer</field> | ||
| <field name="view_mode">list,form</field> | ||
| <field name="domain">[('property_type_id', '=', active_id)]</field> | ||
| </record> | ||
|
|
||
| <record id="estate_property_offer_form" model="ir.ui.view"> | ||
| <field name="name">estate.property.offer.form</field> | ||
| <field name="model">estate.property.offer</field> | ||
| <field name="arch" type="xml"> | ||
| <form string="Offer"> | ||
| <sheet> | ||
| <group> | ||
| <field name="price" string="Price"/> | ||
| <field name="state" string="Status"/> | ||
| <field name="partner_id" string="Partner"/> | ||
| <field name="validaty" string="Validity (days)"/> | ||
| <field name="date_deadline" string="Offer Deadline"/> | ||
| </group> | ||
| </sheet> | ||
| </form> | ||
| </field> | ||
| </record> | ||
|
|
||
| <record id="estate_property_offer_list" model="ir.ui.view"> | ||
| <field name="name">estate.property.offer.list</field> | ||
| <field name="model">estate.property.offer</field> | ||
| <field name="arch" type="xml"> | ||
| <list string="Offers" editable="bottom" decoration-success="state == 'accepted'" decoration-danger="state == 'refused'"> | ||
| <field name="price" string="Price"/> | ||
| <button name="accept_offer" type="object" string="Accept" icon="fa-check" invisible="state == 'accepted' or state == 'refused'"/> | ||
| <button name="refuse_offer" type="object" string="Refuse" icon="fa-close" invisible="state == 'accepted' or state == 'refused'"/> | ||
| <field name="partner_id" string="Partner"/> | ||
| </list> | ||
| </field> | ||
| </record> | ||
| </odoo> |
Uh oh!
There was an error while loading. Please reload this page.