Skip to content

Commit 6dc2b32

Browse files
committed
[ADD] estate: added module logic and basic functionalities
1 parent b158e66 commit 6dc2b32

16 files changed

+412
-170
lines changed

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

estate/__manifest__.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,20 @@
1-
# -*- coding: utf-8 -*-
21
{
32
'name': "Real Estate",
43
'summary': """,
54
'description': """,
6-
'version': '0.1',
5+
'version': "0.1",
76
'application': True,
8-
'category': 'Tutorials/estate',
7+
'category': "Tutorials/estate",
98
'installable': True,
10-
'depends': ['web', 'contacts'],
9+
'depends': ["web", "contacts"],
1110
'data': [
12-
'access/ir.model.access.csv',
13-
'views/estate_property_views.xml',
14-
'views/estate_property_type_views.xml',
15-
'views/estate_property_tag_views.xml',
16-
'views/estate_property_offer_views.xml',
17-
'views/res_users_views.xml',
18-
'views/estate_menus.xml'
11+
"security/ir.model.access.csv",
12+
"views/estate_property_views.xml",
13+
"views/estate_property_type_views.xml",
14+
"views/estate_property_tag_views.xml",
15+
"views/estate_property_offer_views.xml",
16+
"views/res_users_views.xml",
17+
"views/estate_menus.xml",
1918
],
20-
'license': 'AGPL-3'
19+
'license': "AGPL-3",
2120
}

estate/models/__init__.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
from . import estate_property
2-
from . import estate_property_type
3-
from . import estate_property_tag
4-
from . import estate_property_offer
5-
from . import res_users
1+
from . import (
2+
estate_property,
3+
estate_property_offer,
4+
estate_property_tag,
5+
estate_property_type,
6+
res_users,
7+
)

estate/models/estate_property.py

Lines changed: 130 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,107 +1,170 @@
1-
from odoo import models, fields, api, exceptions
2-
from odoo.tools import float_utils # type: ignore
31
from datetime import date
2+
43
from dateutil.relativedelta import relativedelta
54

5+
from odoo import _, api, exceptions, fields, models
6+
from odoo.tools import float_utils
7+
8+
69
class EstateProperty(models.Model):
7-
_name = "estate.property"
10+
# ----------------------------------------
11+
# Private attributes
12+
# ----------------------------------------
13+
_name = 'estate.property'
814
_description = "Comprehensive platform for managing properties, sales, rentals, and client relationships throughout their entire lifecycle."
915
_sql_constraints = [
10-
('check_expected_price_positive', 'CHECK (expected_price > 0)', 'Expected price must be positive.'),
11-
('check_selling_price_positive', 'CHECK (selling_price >= 0)', 'Selling price must be positive.')
16+
(
17+
'check_expected_price_positive',
18+
'CHECK (expected_price > 0)',
19+
"Expected price must be positive.",
20+
),
21+
(
22+
'check_selling_price_positive',
23+
'CHECK (selling_price >= 0)',
24+
"Selling price must be positive.",
25+
),
1226
]
13-
_order = "id desc"
27+
_order = 'id desc'
1428

29+
# ----------------------------------------
30+
# Field declarations
31+
# ----------------------------------------
1532
name = fields.Char(required=True)
1633
description = fields.Text()
1734
postcode = fields.Char()
18-
date_availability = fields.Date(copy=False, default=lambda self: date.today() + relativedelta(months=3))
35+
date_availability = fields.Date(
36+
copy=False, default=lambda self: date.today() + relativedelta(months=3)
37+
)
1938
expected_price = fields.Float(required=True)
2039
selling_price = fields.Float(readonly=True, copy=False)
21-
@api.constrains('selling_price', 'expected_price')
22-
def _check_selling_price(self):
23-
for record in self:
24-
if record.selling_price !=0 and float_utils.float_compare(
25-
record.selling_price,
26-
record.expected_price * 0.9,
27-
precision_digits=2
28-
) < 0:
29-
raise exceptions.ValidationError("Selling price must be greater than or equal to 90% of expected price.")
3040
bedrooms = fields.Integer(default=2)
3141
living_area = fields.Integer()
3242
facades = fields.Integer()
3343
garage = fields.Boolean()
34-
3544
garden = fields.Boolean()
36-
@api.onchange("garden")
37-
def _onchange_garden(self):
38-
if not self.garden:
39-
self.garden_area = 0
40-
self.garden_orientation = False
41-
4245
garden_area = fields.Integer()
4346
garden_orientation = fields.Selection(
44-
string='Garden Orientation',
45-
selection=[('n', 'North'),
46-
('s', 'South'),
47-
('e', 'East'),
48-
('w', 'West')]
47+
string="Garden Orientation",
48+
selection=[
49+
('n', "North"),
50+
('s', "South"),
51+
('e', "East"),
52+
('w', "West"),
53+
],
4954
)
5055
active = fields.Boolean(default=True)
5156
state = fields.Selection(
52-
string='Status',
53-
selection=[('new', 'New'),
54-
('offer_received', 'Offer Received'),
55-
('offer_accepted', 'Offer Accepted'),
56-
('sold', 'Sold'),
57-
('cancelled', 'Cancelled')],
57+
string="Status",
58+
selection=[
59+
('new', "New"),
60+
('offer_received', "Offer Received"),
61+
('offer_accepted', "Offer Accepted"),
62+
('sold', "Sold"),
63+
('cancelled', "Cancelled"),
64+
],
5865
default='new',
5966
required=True,
60-
copy=False
67+
copy=False,
6168
)
62-
63-
buyer_id = fields.Many2one('res.partner', string='Buyer', index=True, copy=False)
64-
salesperson_id = fields.Many2one('res.users', string='Salesperson', index=True, default=lambda self: self.env.user)
65-
66-
tag_ids = fields.Many2many('estate.property.tag', string='Tags')
69+
buyer_id = fields.Many2one(
70+
'res.partner', string="Buyer", index=True, copy=False
71+
)
72+
salesperson_id = fields.Many2one(
73+
'res.users',
74+
string="Salesperson",
75+
index=True,
76+
default=lambda self: self.env.user,
77+
)
78+
tag_ids = fields.Many2many('estate.property.tag', string="Tags")
6779
offer_ids = fields.One2many('estate.property.offer', 'property_id')
68-
type_id = fields.Many2one('estate.property.type', string='Property Type')
69-
80+
type_id = fields.Many2one('estate.property.type', string="Property Type")
7081
total_area = fields.Float(
71-
compute='_compute_total_area',
72-
string='Total Area'
82+
compute="_compute_total_area"
83+
)
84+
best_price = fields.Float(
85+
compute="_compute_best_price", readonly=True
7386
)
87+
88+
# ----------------------------------------
89+
# Compute, inverse and search methods
90+
# ----------------------------------------
7491
@api.depends('living_area', 'garden_area')
7592
def _compute_total_area(self):
76-
for record in self:
77-
record.total_area = record.living_area + record.garden_area if record.garden_area else record.living_area
93+
for estate in self:
94+
estate.total_area = estate.living_area + estate.garden_area
7895

79-
best_price = fields.Float(
80-
compute='_compute_best_price',
81-
string='Best Price',
82-
readonly=True
83-
)
8496
@api.depends('offer_ids.price')
8597
def _compute_best_price(self):
86-
for record in self:
87-
record.best_price = max(record.offer_ids.mapped('price'), default=0.0)
98+
for estate in self:
99+
estate.best_price = max(
100+
estate.offer_ids.mapped('price'), default=0.0
101+
)
102+
103+
# ----------------------------------------
104+
# Constrains and onchange methods
105+
# ----------------------------------------
106+
@api.constrains('selling_price', 'expected_price')
107+
def _check_selling_price(self):
108+
for estate in self:
109+
if (
110+
estate.selling_price != 0
111+
and float_utils.float_compare(
112+
estate.selling_price,
113+
estate.expected_price * 0.9,
114+
precision_digits=2,
115+
)
116+
< 0
117+
):
118+
raise exceptions.ValidationError(
119+
_(
120+
"Selling price must be greater than or equal to 90% of expected price."
121+
)
122+
)
123+
124+
@api.onchange('garden')
125+
def _onchange_garden(self):
126+
if self.garden:
127+
if not self.garden_area:
128+
self.garden_area = 10
129+
if not self.garden_orientation:
130+
self.garden_orientation = 's'
131+
else:
132+
self.garden_area = 0
133+
self.garden_orientation = False
134+
135+
# ----------------------------------------
136+
# CRUD methods (ORM overrides)
137+
# ----------------------------------------
138+
@api.ondelete(at_uninstall=False)
139+
def _unlink_if_user_new_cancelled(self):
140+
for estate in self:
141+
if estate.state not in ['new', 'cancelled']:
142+
raise exceptions.UserError(
143+
_("Can't delete an active property!")
144+
)
88145

146+
# ----------------------------------------
147+
# Action methods
148+
# ----------------------------------------
89149
def action_sold_property(self):
90-
for record in self:
91-
if record.state == 'cancelled':
92-
raise exceptions.UserError("Cannot set as sold if cancelled")
93-
record.state = 'sold'
150+
for estate in self:
151+
if estate.state == 'cancelled':
152+
raise exceptions.UserError(_("Cannot set as sold if cancelled"))
153+
estate.state = 'sold'
154+
# Set buyer from accepted offer
155+
accepted_offer = estate.offer_ids.filtered(
156+
lambda o: o.status == 'accepted'
157+
)
158+
if accepted_offer:
159+
estate.buyer_id = accepted_offer.partner_id
160+
# Ensure selling price is set from the accepted offer
161+
if not estate.selling_price:
162+
estate.selling_price = accepted_offer.price
94163
return True
95164

96165
def action_cancel_property(self):
97-
for record in self:
98-
if record.state == 'sold':
99-
raise exceptions.UserError("Cannot cancel if already sold")
100-
record.state = 'cancelled'
166+
for estate in self:
167+
if estate.state == 'sold':
168+
raise exceptions.UserError(_("Cannot cancel if already sold"))
169+
estate.state = 'cancelled'
101170
return True
102-
103-
@api.ondelete(at_uninstall=False)
104-
def _unlink_if_user_new_cancelled(self):
105-
for record in self:
106-
if record.state not in ['new', 'cancelled']:
107-
raise exceptions.UserError("Can't delete an active property!")

0 commit comments

Comments
 (0)