Skip to content

Commit e06b60d

Browse files
committed
[ADD] estate: Add data validation rules for prices and offer acceptance
- Prevents negative or zero amounts for property and offer prices - Ensures property types and tags must be unique - Blocks accepting offers lower than 90% of the expected price
1 parent d62d928 commit e06b60d

File tree

5 files changed

+132
-75
lines changed

5 files changed

+132
-75
lines changed

estate/__manifest__.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
{
22
"name": "Real Estate",
33
"description": "",
4-
"depends": ['base'],
4+
"depends": ["base"],
55
"sequence": 1,
6-
"data" : [
7-
'security/ir.model.access.csv',
8-
'views/estate_property_views.xml',
9-
'views/estate_property_type_views.xml',
10-
'views/estate_property_tag_views.xml',
11-
'views/estate_property_offer_views.xml',
12-
'views/estate_menus.xml',
6+
"data": [
7+
"security/ir.model.access.csv",
8+
"views/estate_property_views.xml",
9+
"views/estate_property_type_views.xml",
10+
"views/estate_property_tag_views.xml",
11+
"views/estate_property_offer_views.xml",
12+
"views/estate_menus.xml",
1313
],
1414
"license": "LGPL-3",
1515
"application": True,
16-
"installable": True
17-
}
16+
"installable": True,
17+
}

estate/models/estate_property.py

Lines changed: 74 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,43 @@
11
from odoo import models, fields, api
2+
from odoo.exceptions import ValidationError
23
from odoo.exceptions import UserError
4+
from odoo.tools import float_is_zero, float_compare
35
from datetime import timedelta
46

7+
58
class EstateProperty(models.Model):
6-
_name = 'estate.property'
7-
_description = 'Estate Property'
9+
_name = "estate.property"
10+
_description = "Estate Property"
811

9-
name = fields.Char(string="Name",required=True)
12+
name = fields.Char(string="Name", required=True)
1013
description = fields.Text(string="Description")
1114
active = fields.Boolean(
1215
string="Active",
13-
default=True,
14-
help="Mark as active if you want the property to be listed.",)
16+
default=True,
17+
help="Mark as active if you want the property to be listed.",
18+
)
1519
postcode = fields.Char(string="Postcode")
1620
property_type_id = fields.Many2one("estate.property.type", string="Property Type")
1721
date_availability = fields.Date(
18-
string='Available From',
19-
default= fields.Date.today() + timedelta(days=90),
20-
copy=False)
21-
expected_price = fields.Float(string="Expected Price",required=True)
22-
selling_price = fields.Float(string="Selling Price" ,readonly=True ,copy=False)
23-
bedrooms = fields.Integer(string="Bedrooms",default=2)
22+
string="Available From",
23+
default=fields.Date.today() + timedelta(days=90),
24+
copy=False,
25+
)
26+
expected_price = fields.Float(string="Expected Price", required=True)
27+
selling_price = fields.Float(string="Selling Price", readonly=True, copy=False)
28+
bedrooms = fields.Integer(string="Bedrooms", default=2)
2429
living_area = fields.Integer(string="Living Area (sqm)")
2530
facades = fields.Integer(string="Facades")
2631
garage = fields.Boolean(string="Garage")
2732
garden = fields.Boolean(string="Garden")
2833
garden_area = fields.Integer(string="Garden Area (sqm)")
2934
garden_orientation = fields.Selection(
30-
string='Garden Orientation',
31-
selection = [
32-
('north', 'North'),
33-
('south', 'South'),
34-
('east', 'East'),
35-
('west', 'West')
35+
string="Garden Orientation",
36+
selection=[
37+
("north", "North"),
38+
("south", "South"),
39+
("east", "East"),
40+
("west", "West"),
3641
],
3742
)
3843
total_area = fields.Integer(
@@ -55,29 +60,32 @@ class EstateProperty(models.Model):
5560
salesman_id = fields.Many2one(
5661
"res.users", string="Salesman", default=lambda self: self.env.user
5762
)
58-
buyer_id = fields.Many2one("res.partner", string="Buyer", copy = False)
63+
buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False)
5964

6065
tag_ids = fields.Many2many(
61-
'estate.property.tag',
62-
string='Tags',
63-
help='Properties associated with this tag.'
64-
)
66+
"estate.property.tag",
67+
string="Tags",
68+
help="Properties associated with this tag.",
69+
)
6570

6671
offer_ids = fields.One2many(
67-
'estate.property.offer',
68-
'property_id',
69-
string='Offers',
70-
help='Offers made on this property.')
71-
72+
"estate.property.offer",
73+
"property_id",
74+
string="Offers",
75+
help="Offers made on this property.",
76+
)
77+
7278
best_price = fields.Float(
73-
string = "Best Offer",
74-
compute = "_compute_best_price",
79+
string="Best Offer",
80+
compute="_compute_best_price",
7581
)
76-
77-
@api.depends('living_area', 'garden_area', 'garden')
82+
83+
@api.depends("living_area", "garden_area", "garden")
7884
def _compute_total_area(self):
7985
for property in self:
80-
property.total_area = property.living_area + (property.garden_area if property.garden else 0)
86+
property.total_area = property.living_area + (
87+
property.garden_area if property.garden else 0
88+
)
8189

8290
@api.depends("offer_ids.price")
8391
def _compute_best_price(self):
@@ -89,27 +97,55 @@ def _onchange_garden(self):
8997
for property in self:
9098
if property.garden:
9199
property.garden_area = 10
92-
property.garden_orientation = 'north'
100+
property.garden_orientation = "north"
93101
else:
94102
property.garden_area = 0
95103
property.garden_orientation = False
96104

97105
def action_set_sold(self):
98106
for property in self:
99107
if property.selling_price > 0.0 and property.state != "cancelled":
100-
property.state = 'sold'
108+
property.state = "sold"
101109
elif property.state == "cancelled":
102110
raise UserError("A cancelled property cannot be sold.")
103111
elif property.state == "new" or property.state == "offer received":
104-
raise UserError("This property must have an accepted offer before it can be sold.")
112+
raise UserError(
113+
"This property must have an accepted offer before it can be sold."
114+
)
105115
elif property.state == "sold":
106116
raise UserError("This property is already sold.")
107-
117+
108118
def action_set_cancelled(self):
109119
for property in self:
110120
if property.state != "cancelled" and property.state != "sold":
111-
property.state = 'cancelled'
121+
property.state = "cancelled"
112122
elif property.state == "cancelled":
113123
raise UserError("This property is already cancelled.")
114124
elif property.state == "sold":
115-
raise UserError("A sold property cannot be cancelled.")
125+
raise UserError("A sold property cannot be cancelled.")
126+
127+
_sql_constraints = [
128+
(
129+
"check_expected_price",
130+
"CHECK(expected_price > 0)",
131+
"The expected price must be greater than 0.",
132+
),
133+
]
134+
135+
@api.constrains("selling_price", "expected_price")
136+
def _check_selling_price(self):
137+
for property in self:
138+
if float_is_zero(property.selling_price, precision_rounding=2):
139+
continue
140+
141+
if (
142+
float_compare(
143+
property.selling_price,
144+
property.expected_price * 0.9,
145+
precision_rounding=2,
146+
)
147+
< 0
148+
):
149+
raise ValidationError(
150+
"The selling price cannot be lower than 90'%' of the expected price!"
151+
)

estate/models/estate_property_offer.py

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,47 +2,55 @@
22
from odoo.exceptions import UserError
33
from datetime import timedelta
44

5+
56
class EstatePropertyOffer(models.Model):
6-
_name = 'estate.property.offer'
7-
_description = 'Estate Property Offer'
7+
_name = "estate.property.offer"
8+
_description = "Estate Property Offer"
89

910
price = fields.Float(string="Price")
1011
status = fields.Selection(
1112
string="Status",
1213
selection=[
13-
('accepted', 'Accepted'),
14-
('refused', 'Refused'),
15-
]
14+
("accepted", "Accepted"),
15+
("refused", "Refused"),
16+
],
1617
)
1718
validity = fields.Integer(
18-
string = "Validity (days)",
19+
string="Validity (days)",
1920
default=7,
20-
help="Validity of the offer in days, after that it will be refused automatically."
21+
help="Validity of the offer in days, after that it will be refused automatically.",
2122
)
2223
date_deadline = fields.Date(
23-
string = "Deadline",
24+
string="Deadline",
2425
compute="_compute_date_deadline",
2526
inverse="_inverse_date_deadline",
2627
store=True,
2728
)
28-
partner_id = fields.Many2one("res.partner", string="Partner" , required=True)
29-
property_id = fields.Many2one("estate.property",string="Property", required=True)
29+
partner_id = fields.Many2one("res.partner", string="Partner", required=True)
30+
property_id = fields.Many2one("estate.property", string="Property", required=True)
3031

31-
@api.depends('validity')
32+
@api.depends("validity")
3233
def _compute_date_deadline(self):
3334
for offer in self:
34-
base_date = fields.Date.to_date(offer.create_date) if offer.create_date else fields.Date.context_today(offer)
35+
base_date = (
36+
fields.Date.to_date(offer.create_date)
37+
if offer.create_date
38+
else fields.Date.context_today(offer)
39+
)
3540
offer.date_deadline = base_date + timedelta(days=offer.validity or 0)
3641

3742
def _inverse_date_deadline(self):
3843
for offer in self:
3944
if offer.date_deadline:
40-
base_date = fields.Date.to_date(offer.create_date) if offer.create_date else fields.Date.context_today(offer)
45+
base_date = (
46+
fields.Date.to_date(offer.create_date)
47+
if offer.create_date
48+
else fields.Date.context_today(offer)
49+
)
4150
offer.validity = (offer.date_deadline - base_date).days
4251
else:
4352
offer.validity = 0
4453

45-
4654
# @api.onchange("date_deadline")
4755
# def _onchange_date_deadline(self):
4856
# for offer in self:
@@ -55,23 +63,27 @@ def _inverse_date_deadline(self):
5563
def action_set_accepted(self):
5664
for offer in self:
5765
if offer.property_id.selling_price == 0.0:
58-
offer.status = 'accepted'
59-
offer.property_id.state = 'offer accepted'
66+
offer.status = "accepted"
67+
offer.property_id.state = "offer accepted"
6068
offer.property_id.selling_price = offer.price
6169
offer.property_id.buyer_id = offer.partner_id
6270

6371
other_offers = offer.property_id.offer_ids - offer
6472

65-
other_offers.write({'status': 'refused'})
73+
other_offers.write({"status": "refused"})
6674
else:
6775
raise UserError("One offer is already accepted for this property.")
68-
76+
6977
def action_set_refused(self):
7078
for offer in self:
71-
if offer.status == 'accepted':
72-
offer.status = 'refused'
73-
offer.property_id.state = 'offer received'
79+
if offer.status == "accepted":
80+
offer.status = "refused"
81+
offer.property_id.state = "offer received"
7482
offer.property_id.buyer_id = False
7583
offer.property_id.selling_price = 0.0
7684
else:
77-
raise UserError("This offer is not accepted, so it cannot be refused.")
85+
raise UserError("This offer is not accepted, so it cannot be refused.")
86+
87+
_sql_constraints = [
88+
("check_price", "CHECK(price > 0)", "The price must be greater than 0."),
89+
]
Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
from odoo import models, fields
22

3+
34
class EstatePropertyTag(models.Model):
4-
_name = 'estate.property.tag'
5-
_description = 'Estate Property Tag'
5+
_name = "estate.property.tag"
6+
_description = "Estate Property Tag"
67

78
name = fields.Char(string="Tag Name", required=True)
89
description = fields.Text(string="Description")
9-
10+
11+
_sql_constraints = [
12+
("unique_name", "UNIQUE(name)", "A tag with same name is already exists."),
13+
]
Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
from odoo import models, fields
22

3+
34
class EstatePropertyType(models.Model):
4-
_name = 'estate.property.type'
5-
_description = 'Estate Property Type'
5+
_name = "estate.property.type"
6+
_description = "Estate Property Type"
67

78
name = fields.Char(required=True)
9+
10+
_sql_constraints = [
11+
("unique_name", "UNIQUE(name)", "A type with same name is already exists."),
12+
]

0 commit comments

Comments
 (0)