Skip to content

Commit 63322ee

Browse files
committed
[IMP] real_estate: restrict access date and define module data
This tutorial helped implement strong security in the Real Estate module. We learned to restrict access using user groups, access rights, and record rules. Real estate agents can only manage their own properties, not others’. Invoices can be created safely by bypassing access with proper checks. Multi-company rules and visibility settings improve data privacy and user experience. This tutorial explained how to define and load different types of data in an Odoo module. We learned the difference between master data (essential for the module to work) and demo data (useful for testing and demonstration). Data can be added using CSV for simple entries and XML for more complex or dynamic records. We used eval, ref, search, and function to assign computed values, link existing records, and run logic on data load. We also learned to use X2many fields, how to extend data, and how to securely access and reference records programmatically.
1 parent baf7908 commit 63322ee

18 files changed

+225
-45
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,6 @@ dmypy.json
127127

128128
# Pyre type checker
129129
.pyre/
130+
131+
# Ignore VS Code settings
132+
.vscode/

estate_account/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
# License LGPL-3
12
from . import models

estate_account/__manifest__.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,9 @@
22
'name': "Account",
33
'version': '1.0',
44
'depends': ['base', 'estate_gasa', 'account'],
5-
'author': "Author Name",
5+
'author': "gasa",
66
'category': 'Category',
77
"license": "LGPL-3",
88
"application": True,
9-
"sequence": 1,
10-
# 'data': [
11-
# 'views/estate_property_views.xml',
12-
# ],
9+
"sequence": 1
1310
}

estate_account/models/estate.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,23 @@
55
class Estate(models.Model):
66
_inherit = 'estate.property'
77

8-
def action_mark_sold(self):
8+
def action_mark_sold(self):
9+
self.check_access_rights('write')
10+
self.check_access_rule('write')
11+
12+
print(" REACHED ".center(100, '='))
913
res = super().action_mark_sold()
1014

15+
journal = self.env['account.journal'].sudo().search([('type', '=', 'sale')], limit=1)
16+
if not journal:
17+
raise UserError("No sale journal found. Please configure at least one sale journal.")
18+
1119
for record in self:
1220
if not record.buyer:
1321
raise UserError("Please set a Buyer before generating an invoice.")
1422
if not record.selling_price:
1523
raise UserError("Please set a Selling Price before generating an invoice.")
1624

17-
journal = self.env['account.journal'].search([('type', '=', 'sale')], limit=1)
18-
if not journal:
19-
raise UserError("No sale journal found. Please configure at least one sale journal.")
20-
2125
invoice_vals = {
2226
"partner_id": record.buyer.id,
2327
"move_type": "out_invoice",
@@ -36,6 +40,6 @@ def action_mark_sold(self):
3640
]
3741
}
3842

39-
self.env["account.move"].create(invoice_vals)
43+
self.env["account.move"].sudo().create(invoice_vals)
4044

4145
return res

estate_gasa/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
# License LGPL-3
12
from . import models

estate_gasa/__manifest__.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,24 @@
22
'name': "estate",
33
'version': '1.0',
44
'depends': ['base'],
5-
'author': "Author Name",
6-
'category': 'Category',
5+
'author': "gasa",
6+
'category': 'Real Estate/Brokerage',
77
"license": "LGPL-3",
88
"application": True,
99
"sequence": 1,
1010
'data': [
11-
'security/ir.model.access.csv',
12-
'views/estate_property_views.xml',
13-
'views/estate_property_offer_views.xml',
14-
'views/estate_property_type_views.xml',
15-
'views/estate_tag_views.xml',
16-
'views/inherited_model.xml',
17-
'views/estate_menus.xml',
11+
'security/security.xml',
12+
'security/ir.model.access.csv',
13+
'security/estate_property_rules.xml',
14+
'data/estate.property.type.csv',
15+
'views/estate_property_views.xml',
16+
'views/estate_property_offer_views.xml',
17+
'views/estate_property_type_views.xml',
18+
'views/estate_tag_views.xml',
19+
'views/inherited_model.xml',
20+
'views/estate_menus.xml'
21+
],
22+
"demo": [
23+
'demo/estate_property_demo_data.xml',
1824
],
1925
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
id,name
2+
estate_property_type_residential,Residential
3+
estate_property_type_commercial,Commercial
4+
estate_property_type_industrial,Industrial
5+
estate_property_type_land,Land
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<odoo>
3+
<record id="demo_property_big_villa" model="estate.property">
4+
<field name="name">Big Villa</field>
5+
<field name="state">new</field>
6+
<field name="description">A nice and big villa</field>
7+
<field name="postcode">12345</field>
8+
<field name="date_availability">2025-07-10</field>
9+
<field name="expected_price">97000</field>
10+
<field name="selling_price">97000</field>
11+
<field name="bedrooms">6</field>
12+
<field name="living_area">100</field>
13+
<field name="facades">4</field>
14+
<field name="garage">True</field>
15+
<field name="garden">True</field>
16+
<field name="garden_area">100</field>
17+
<field name="garden_orientation">south</field>
18+
<field name="property_type" ref="estate_property_type_residential" />
19+
</record>
20+
21+
<record id="demo_property_trailer_home" model="estate.property">
22+
<field name="name">Trailer home</field>
23+
<field name="state">cancelled</field>
24+
<field name="description">Home in a trailer park</field>
25+
<field name="postcode">54321</field>
26+
<field name="date_availability">2025-07-10</field>
27+
<field name="expected_price">1000</field>
28+
<field name="selling_price">1000</field>
29+
<field name="bedrooms">2</field>
30+
<field name="living_area">100</field>
31+
<field name="facades">4</field>
32+
<field name="garage">True</field>
33+
<field name="garden">True</field>
34+
<field name="garden_orientation">south</field>
35+
<field name="property_type" ref="estate_property_type_residential" />
36+
</record>
37+
38+
<record id="property_offer_1" model="estate.property.offer">
39+
<field name="partner_id" ref="base.res_partner_2" />
40+
<field name="property_id" ref="demo_property_big_villa" />
41+
<field name="price">90000</field>
42+
<field name="validity">14</field>
43+
<field name="date_deadline" eval="(datetime.now() + timedelta(days=14)).date().isoformat()" />
44+
</record>
45+
46+
<record id="property_offer_2" model="estate.property.offer">
47+
<field name="partner_id" ref="base.res_partner_2" />
48+
<field name="property_id" ref="demo_property_big_villa" />
49+
<field name="price">1500000</field>
50+
<field name="validity">14</field>
51+
<field name="date_deadline" eval="(datetime.now() + timedelta(days=14)).date().isoformat()" />
52+
</record>
53+
54+
<record id="property_offer_3" model="estate.property.offer">
55+
<field name="partner_id" ref="base.res_partner_3" />
56+
<field name="property_id" ref="demo_property_big_villa" />
57+
<field name="price">1500001</field>
58+
<field name="validity">14</field>
59+
<field name="date_deadline" eval="(datetime.now() + timedelta(days=14)).date().isoformat()" />
60+
</record>
61+
<function model="estate.property.offer" name="action_accept">
62+
<value eval="[ref('property_offer_1')]" />
63+
</function>
64+
65+
<function model="estate.property.offer" name="action_refuse">
66+
<value eval="[ref('property_offer_2')]" />
67+
</function>
68+
69+
<function model="estate.property.offer" name="action_refuse">
70+
<value eval="[ref('property_offer_3')]" />
71+
</function>
72+
73+
<record id="demo_property_with_inline_offers" model="estate.property">
74+
<field name="name">Estate with Inline Offers</field>
75+
<field name="state">new</field>
76+
<field name="postcode">44444</field>
77+
<field name="expected_price">120000</field>
78+
<field name="date_availability">2025-07-15</field>
79+
<field name="garden">True</field>
80+
<field name="garage">True</field>
81+
<field name="bedrooms">3</field>
82+
<field name="living_area">95</field>
83+
<field name="property_type" ref="estate_property_type_residential" />
84+
<field name="offer_ids"
85+
eval="[
86+
Command.create({
87+
'partner_id': ref('base.res_partner_2'),
88+
'price': 100000,
89+
'validity': 10,
90+
}),
91+
Command.create({
92+
'partner_id': ref('base.res_partner_3'),
93+
'price': 115000,
94+
'validity': 14,
95+
})
96+
]" />
97+
</record>
98+
</odoo>

estate_gasa/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# License LGPL-3
12
from . import estate
23
from . import estate_property_type
34
from . import estate_property_tag

estate_gasa/models/estate.py

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ class Estate(models.Model):
2020
active = fields.Boolean(default=True)
2121
living_area = fields.Integer(string="Living Area")
2222
facades = fields.Integer(string="Facades")
23-
garden = fields.Boolean(string="Garage")
23+
garage = fields.Boolean(string="Garage")
24+
garden = fields.Boolean(string="Garden")
2425
garden_area = fields.Integer(string="Garden Area")
2526
garden_orientation = fields.Selection(
2627
[
@@ -64,16 +65,23 @@ class Estate(models.Model):
6465
store=True
6566
)
6667

67-
@api.depends('living_area', 'garden_area')
68-
def _compute_total_area(self):
69-
for record in self:
70-
record.total_area = record.living_area + record.garden_area
71-
7268
best_price = fields.Float(
7369
string="Best Offer",
7470
compute="_compute_best_price"
7571
)
72+
7673
selling_price = fields.Float(copy=False)
74+
company_id = fields.Many2one(
75+
'res.company',
76+
string='Company',
77+
required=True,
78+
default=lambda self: self.env.company
79+
)
80+
81+
@api.depends('living_area', 'garden_area')
82+
def _compute_total_area(self):
83+
for record in self:
84+
record.total_area = record.living_area + record.garden_area
7785

7886
@api.depends("offer_ids.price")
7987
def _compute_best_price(self):
@@ -102,11 +110,6 @@ def action_mark_cancelled(self):
102110
raise UserError("Sold properties cannot be canceled.")
103111
record.state = 'cancelled'
104112

105-
_sql_constraints = [
106-
('check_expected_price_positive', 'CHECK(expected_price > 0)', 'The expected price must be strictly positive.'),
107-
('check_selling_price_positive', 'CHECK(selling_price >= 0)', 'The selling price must be positive.'),
108-
]
109-
110113
@api.constrains('selling_price', 'expected_price')
111114
def _check_selling_price_threshold(self):
112115
for record in self:
@@ -127,3 +130,8 @@ def _check_property_state_before_delete(self):
127130
for record in self:
128131
if record.state not in ['new', 'cancelled']:
129132
raise UserError("You can only delete properties that are in 'New' or 'Cancelled' state.")
133+
134+
_sql_constraints = [
135+
('check_expected_price_positive', 'CHECK(expected_price > 0)', 'The expected price must be strictly positive.'),
136+
('check_selling_price_positive', 'CHECK(selling_price >= 0)', 'The selling price must be positive.'),
137+
]

0 commit comments

Comments
 (0)