Skip to content

Commit 8f87fae

Browse files
committed
[IMP] real_estate: add SQL constraints and UI improvements for property models
This commit adds data integrity and UI improvements as guided by Chapters 10 and 11 of the Odoo Server Framework 101 tutorial. Constraints: - Added SQL constraints to ensure unique names for property types and tags. - Added SQL constraint to ensure color index stays within valid range. - Implemented Python constraints to enforce logical business rules UI Enhancements: - Introduced computed field on property types. - Added stat button to open related offers via domain. - Enhanced form views with notebooks and inline editing for better UX. - Updated navigation menu to include property type and tag settings.
1 parent 1601aac commit 8f87fae

13 files changed

+245
-106
lines changed

real_estate/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
from . import models
1+
from . import models

real_estate/__manifest__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99
'author': "prbo",
1010
'data': [
1111
'security/ir.model.access.csv',
12-
'views/estate_property_views.xml',
12+
'views/estate_property_offer_view.xml',
1313
'views/estate_property_type_view.xml',
14+
'views/estate_property_views.xml',
1415
'views/estate_property_tag_view.xml',
15-
'views/estate_property_offer_view.xml',
1616
'views/estate_menus.xml'
1717
]
18-
}
18+
}

real_estate/models/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
from . import estate_property
22
from . import estate_property_type
33
from . import estate_property_tag
4-
from . import estate_property_offer
4+
from . import estate_property_offer

real_estate/models/estate_property.py

Lines changed: 44 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
from odoo import fields, models, api
22
from datetime import date, timedelta
3-
from odoo.exceptions import UserError
3+
from odoo.exceptions import UserError, ValidationError
4+
from odoo.tools.float_utils import float_compare
45

5-
class estate_property(models.Model):
6+
7+
class EstateProperty(models.Model):
68
_name = "estate.property"
79
_description = "Test Model"
8-
10+
_order = "id desc"
911

1012
name = fields.Char(required=True)
1113
description = fields.Text()
@@ -17,56 +19,64 @@ class estate_property(models.Model):
1719
)
1820
expected_price = fields.Float(required=True)
1921
selling_price = fields.Float(readonly=True, copy=False)
20-
2122
best_price = fields.Float(compute="_compute_best_price")
2223

2324
bedrooms = fields.Integer(default=2)
24-
2525
facades = fields.Integer()
2626
garage = fields.Boolean()
2727
garden = fields.Boolean()
2828
living_area = fields.Float()
2929
garden_area = fields.Float()
3030
total_area = fields.Float(compute="_compute_total_area")
31+
3132
garden_orientation = fields.Selection(
3233
string='Garden Orientation',
33-
selection=[('north', 'North'), ('south', 'South'),
34-
('east', 'East'), ('west', 'West')],
34+
selection=[
35+
('north', 'North'),
36+
('south', 'South'),
37+
('east', 'East'),
38+
('west', 'West')
39+
],
3540
help="Direction of the garden"
3641
)
42+
3743
active = fields.Boolean(default=True)
3844
state = fields.Selection(
39-
selection=[
40-
('new', 'New'),
41-
('offer_received', 'Offer Received'),
42-
('offer_accepted', 'Offer Accepted'),
43-
('sold', 'Sold'),
44-
('cancelled', 'Cancelled')
45-
],
46-
required=True,
47-
copy=False,
48-
default='new'
49-
)
50-
property_type = fields.Many2one("estate.property.type", string="Property Type", default=None)
45+
selection=[
46+
('new', 'New'),
47+
('offer_received', 'Offer Received'),
48+
('offer_accepted', 'Offer Accepted'),
49+
('sold', 'Sold'),
50+
('cancelled', 'Cancelled')
51+
],
52+
required=True,
53+
copy=False,
54+
default='new'
55+
)
56+
57+
property_type = fields.Many2one("estate.property.type", string="Property Type")
5158
buyer = fields.Many2one("res.partner", string="Buyer", copy=False)
5259
seller = fields.Many2one("res.partner", string="Salesperson", default=lambda self: self.env.user)
5360
tag_ids = fields.Many2many("estate.property.tag", string="Tags", default=None)
5461
offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers")
5562

63+
_sql_constraints = [
64+
('check_expected_price_positive', 'CHECK(expected_price > 0)', 'Expected price must be strictly positive.'),
65+
('check_selling_price_positive', 'CHECK(selling_price >= 0)', 'Selling price must be zero or positive.')
66+
]
67+
5668
@api.depends("living_area", "garden_area")
5769
def _compute_total_area(self):
5870
for record in self:
5971
record.total_area = record.living_area + record.garden_area
6072

61-
6273
@api.depends("offer_ids.price")
6374
def _compute_best_price(self):
6475
for record in self:
6576
if record.offer_ids:
6677
record.best_price = max(record.offer_ids.mapped("price"))
6778
else:
68-
record.best_price = 0.0
69-
79+
record.best_price = 0.0
7080

7181
@api.onchange('garden')
7282
def _onchange_garden(self):
@@ -75,21 +85,26 @@ def _onchange_garden(self):
7585
self.garden_orientation = 'north'
7686
else:
7787
self.garden_area = 0
78-
self.garden_orientation = False
79-
88+
self.garden_orientation = False
8089

8190
def action_sold(self):
8291
for record in self:
83-
if record.state == 'cancelled':
84-
raise UserError("Cancelled property cannot be sold.")
92+
if record.state in ['sold', 'cancelled']:
93+
raise UserError("You cannot sell a cancelled or already sold property.")
8594
record.state = 'sold'
8695
return True
8796

8897
def action_cancel(self):
8998
for record in self:
90-
if record.state == 'sold':
91-
raise UserError("Sold property cannot be cancelled.")
99+
if record.state in ['sold', 'cancelled']:
100+
raise UserError("You cannot cancel a sold or already cancelled property.")
92101
record.state = 'cancelled'
93102
return True
94103

95-
104+
@api.constrains('selling_price', 'expected_price')
105+
def _check_selling_price_margin(self):
106+
for record in self:
107+
if float_compare(record.selling_price, 0.0, precision_digits=2) > 0:
108+
min_acceptable_price = 0.9 * record.expected_price
109+
if float_compare(record.selling_price, min_acceptable_price, precision_digits=2) < 0:
110+
raise ValidationError("Selling price cannot be lower than 90% of the expected price.")

real_estate/models/estate_property_offer.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,43 +2,57 @@
22
from datetime import timedelta
33
from odoo.exceptions import UserError
44

5+
56
class EstatePropertyOffer(models.Model):
67
_name = "estate.property.offer"
78
_description = "Property Offer"
9+
_order = "price desc"
810

9-
price = fields.Float()
11+
price = fields.Float(required=True)
1012
status = fields.Selection([
1113
('accepted', 'Accepted'),
1214
('refused', 'Refused')
1315
])
1416
partner_id = fields.Many2one("res.partner", string="Partner", required=True)
1517
property_id = fields.Many2one("estate.property", string="Property", required=True)
18+
property_type_id = fields.Many2one(
19+
related="property_id.property_type",
20+
store=True,
21+
string="Property Type"
22+
)
1623

1724
validity = fields.Integer(default=7)
18-
date_deadline = fields.Date(compute="_compute_date_deadline", inverse="_inverse_date_deadline")
25+
date_deadline = fields.Date(
26+
compute="_compute_date_deadline",
27+
inverse="_inverse_date_deadline",
28+
store=True
29+
)
30+
31+
_sql_constraints = [
32+
('check_offer_price_positive', 'CHECK(price > 0)', 'Offer price must be strictly positive.')
33+
]
1934

2035
@api.depends("create_date", "validity")
2136
def _compute_date_deadline(self):
2237
for record in self:
2338
create_date = record.create_date or fields.Date.today()
2439
record.date_deadline = create_date + timedelta(days=record.validity)
25-
print("Computed date_deadline:", record.date_deadline)
2640

2741
def _inverse_date_deadline(self):
2842
for record in self:
2943
create_date = record.create_date.date() if record.create_date else fields.Date.today()
3044
record.validity = (record.date_deadline - create_date).days
3145

32-
3346
def action_accept(self):
3447
for record in self:
3548
if record.property_id.state == 'sold':
3649
raise UserError("You cannot accept an offer on a sold property.")
3750
record.status = 'accepted'
3851
record.property_id.selling_price = record.price
39-
record.property_id.buyer = record.partner_id
52+
record.property_id.buyer = record.partner_id
4053
record.property_id.state = 'offer_accepted'
4154

55+
# Refuse all other offers
4256
other_offers = self.search([
4357
('property_id', '=', record.property_id.id),
4458
('id', '!=', record.id)
Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
from odoo import fields, models
22

3-
class estate_property_tag(models.Model):
3+
4+
class EstatePropertyTag(models.Model):
45
_name = "estate.property.tag"
56
_description = "Property Tag"
7+
_order = "name"
68

79
name = fields.Char(required=True)
10+
color = fields.Integer(string="Color")
11+
12+
_sql_constraints = [
13+
('unique_tag_name', 'UNIQUE(name)', 'Tag name must be unique.')
14+
]
Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,36 @@
1-
from odoo import fields, models
1+
from odoo import fields, models, api
22

33

4-
class estate_property_type(models.Model):
4+
class EstatePropertyType(models.Model):
55
_name = "estate.property.type"
6-
_description = "property type"
7-
name = fields.Char(required=True)
6+
_description = "Property Type"
7+
_order = "sequence, name"
8+
9+
name = fields.Char(required=True)
10+
sequence = fields.Integer(default=10)
11+
12+
_sql_constraints = [
13+
('unique_type_name', 'UNIQUE(name)', 'Property type name must be unique.')
14+
]
15+
16+
offer_ids = fields.One2many(
17+
'estate.property.offer',
18+
'property_type_id',
19+
string="Offers"
20+
)
21+
22+
offer_count = fields.Integer(
23+
string="Offer Count",
24+
compute="_compute_offer_count"
25+
)
26+
27+
property_ids = fields.One2many(
28+
"estate.property",
29+
"property_type",
30+
string="Properties"
31+
)
32+
33+
@api.depends('offer_ids')
34+
def _compute_offer_count(self):
35+
for record in self:
36+
record.offer_count = len(record.offer_ids)

real_estate/security/ir.model.access.csv

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
22
access_estate_property_user,access.estate.property,model_estate_property,base.group_user,1,1,1,1
33
access_estate_property_type_user,access.estate.property.type,model_estate_property_type,base.group_user,1,1,1,1
44
access_estate_property_tag_user,access.estate.property.tag,model_estate_property_tag,base.group_user,1,1,1,1
5-
access_estate_property_offer_user,access.estate.property.offer,model_estate_property_offer,base.group_user,1,1,1,1
5+
access_estate_property_offer_user,access.estate.property.offer,model_estate_property_offer,base.group_user,1,1,1,1

real_estate/views/estate_menus.xml

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,34 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<odoo>
3-
<menuitem id="estate_menu_root" name="Estate" sequence="10" />
3+
<menuitem id="estate_menu_root"
4+
name="Estate"
5+
sequence="10" />
46

57
<menuitem id="estate_menu_property"
6-
name="Properties"
7-
parent="estate_menu_root"
8-
sequence="10" />
8+
name="Properties"
9+
parent="estate_menu_root"
10+
sequence="10" />
911

1012
<menuitem id="estate_menu_property_action"
11-
parent="estate_menu_property"
12-
action="action_estate_property"
13-
sequence="10" />
13+
name="All Properties"
14+
parent="estate_menu_property"
15+
action="action_estate_property"
16+
sequence="10" />
1417

15-
<menuitem id="estate_setting" name="Settings" parent="estate_menu_root" />
18+
<menuitem id="estate_setting"
19+
name="Settings"
20+
parent="estate_menu_root"
21+
sequence="20" />
1622

1723
<menuitem id="estate_property_type"
18-
name="Property Types"
19-
parent="estate_setting"
20-
action="action_estate_property_type" />
24+
name="Property Types"
25+
parent="estate_setting"
26+
action="action_estate_property_type"
27+
sequence="10" />
2128

2229
<menuitem id="estate_menu_property_tag"
23-
name="Property Tags"
24-
parent="estate_setting"
25-
action="action_estate_property_tag" />
26-
27-
</odoo>
30+
name="Property Tags"
31+
parent="estate_setting"
32+
action="action_estate_property_tag"
33+
sequence="20" />
34+
</odoo>
Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<odoo>
3-
<record id="view_estate_property_offer_list" model="ir.ui.view">
3+
<record id="view_tree_estate_property_offer_list" model="ir.ui.view">
44
<field name="name">estate.property.offer.list</field>
55
<field name="model">estate.property.offer</field>
66
<field name="arch" type="xml">
7-
<list>
7+
<list string="Offers" editable="bottom">
8+
<field name="property_id" />
89
<field name="price" />
9-
<field name="partner_id" />
1010
<field name="validity" />
1111
<field name="date_deadline" />
1212
<field name="status" />
@@ -19,21 +19,22 @@
1919
<field name="model">estate.property.offer</field>
2020
<field name="arch" type="xml">
2121
<form>
22-
<header>
23-
<button name="action_accept" type="object" string="Accept" class="btn-primary"/>
24-
<button name="action_refuse" type="object" string="Refuse" class="btn-secondary"/>
25-
</header>
26-
2722
<sheet>
2823
<group>
2924
<field name="price" />
30-
<field name="partner_id" />
3125
<field name="validity" />
3226
<field name="date_deadline" />
33-
<field name="status" />
27+
<field name="status" readonly="1" />
3428
</group>
3529
</sheet>
3630
</form>
3731
</field>
3832
</record>
39-
</odoo>
33+
<record id="action_estate_property_offer" model="ir.actions.act_window">
34+
<field name="name">Offers</field>
35+
<field name="res_model">estate.property.offer</field>
36+
<field name="view_mode">list,form</field>
37+
<field name="context">{}</field>
38+
<field name="domain">[('property_type_id', '=', active_id)]</field>
39+
</record>
40+
</odoo>

0 commit comments

Comments
 (0)