Skip to content

Commit 82547cd

Browse files
committed
[ADD] estate: Real Estate App for tutorials
Added Real estate app built up to the end of chapter 11
1 parent b509c0d commit 82547cd

14 files changed

+506
-0
lines changed

estate/__init__.py

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

estate/__manifest__.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
'name': 'Real Estate',
3+
'version': '1.0',
4+
'category': 'Tutorials',
5+
'depends': [
6+
'base'
7+
],
8+
'data': [
9+
'security/ir.model.access.csv',
10+
11+
'views/estate_property_type_views.xml',
12+
'views/estate_property_tag_views.xml',
13+
'views/estate_property_offer_views.xml',
14+
'views/estate_property_views.xml',
15+
'views/estate_menu_views.xml'
16+
],
17+
'installable': True,
18+
'application': True,
19+
'auto_install': False,
20+
'license': 'AGPL-3'
21+
}

estate/models/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from . import property
2+
from . import property_offer
3+
from . import property_tag
4+
from . import property_type

estate/models/property.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
from dateutil.relativedelta import relativedelta
2+
3+
from odoo import models, fields, api
4+
from odoo.exceptions import UserError, ValidationError
5+
from odoo.tools import float_is_zero, float_compare
6+
7+
8+
class Property(models.Model):
9+
_name = 'estate.property'
10+
_description = 'Property'
11+
_sql_constraints = [
12+
('check_expected_price', 'CHECK(expected_price > 0)', 'The Expected Price must be positive.'),
13+
('check_selling_price', 'CHECK(selling_price >= 0)', 'The Selling Price must be positive.'),
14+
('check_bedrooms', 'CHECK(bedrooms >= 0)', 'The number of bedrooms must be positive.'),
15+
('check_living_area', 'CHECK(living_area > 0)', 'The living area must be positive.'),
16+
('check_facades', 'CHECK(facades > 0)', 'The number of facades must be positive.'),
17+
('check_name_unique', 'UNIQUE(name)', 'The Property name must be unique.')
18+
]
19+
_order = 'id desc'
20+
21+
name = fields.Char(string='Title', required=True)
22+
description = fields.Text()
23+
postcode = fields.Char()
24+
date_availability = fields.Date(string='Available From', copy=False,
25+
default=lambda self: fields.Datetime.today() + relativedelta(months=3))
26+
expected_price = fields.Float(required=True)
27+
selling_price = fields.Float(readonly=True, copy=False)
28+
bedrooms = fields.Integer(default=2)
29+
living_area = fields.Integer(string='Living Area (m²)')
30+
facades = fields.Integer()
31+
garage = fields.Boolean()
32+
garden = fields.Boolean()
33+
garden_area = fields.Integer(string='Garden Area (m²)')
34+
garden_orientation = fields.Selection([
35+
('north', 'North'),
36+
('east', 'East'),
37+
('south', 'South'),
38+
('west', 'West')
39+
])
40+
active = fields.Boolean(default=True)
41+
state = fields.Selection([
42+
('new', 'New'),
43+
('offer_received', 'Offer Received'),
44+
('offer_accepted', 'Offer Accepted'),
45+
('sold', 'Sold'),
46+
('canceled', 'Canceled')
47+
], default='new', required=True)
48+
property_type_id = fields.Many2one('estate.property.type', string='Property Type', required=True)
49+
buyer_id = fields.Many2one('res.partner', string='Buyer', copy=False)
50+
salesperson_id = fields.Many2one('res.users', string='Sales Person', default=lambda self: self.env.user)
51+
tag_ids = fields.Many2many('estate.property.tag', string='Tags')
52+
offer_ids = fields.One2many('estate.property.offer', 'property_id', string='Offers')
53+
total_area = fields.Integer(string='Total Area (m²)', compute='_compute_total_area')
54+
best_price = fields.Float(compute='_compute_best_price')
55+
56+
@api.depends('living_area', 'garden_area')
57+
def _compute_total_area(self):
58+
for record in self:
59+
record.total_area = record.living_area + record.garden_area
60+
61+
@api.depends('offer_ids.price')
62+
def _compute_best_price(self):
63+
for record in self:
64+
if len(record.offer_ids) == 0:
65+
record.best_price = 0
66+
else:
67+
record.best_price = max(record.offer_ids.mapped('price'))
68+
69+
@api.onchange('garden')
70+
def _onchange_garden(self):
71+
if self.garden:
72+
self.garden_orientation = 'north'
73+
self.garden_area = 10
74+
else:
75+
self.garden_orientation = None
76+
self.garden_area = 0
77+
78+
@api.constrains('selling_price', 'expected_price')
79+
def _check_date_end(self):
80+
for record in self:
81+
if not float_is_zero(record.selling_price, precision_digits=2) and float_compare(record.selling_price,
82+
0.9 * record.expected_price,
83+
precision_digits=2) >= 0:
84+
raise ValidationError("The selling price must not be below 90% of the expected price.")
85+
86+
def action_sold(self):
87+
for record in self:
88+
if record.state in ['sold', 'canceled']:
89+
raise UserError('Cannot mark as sold.')
90+
record.state = 'sold'
91+
return True
92+
93+
def action_canceled(self):
94+
for record in self:
95+
if record.state in ['sold', 'canceled']:
96+
raise UserError('Cannot mark as canceled.')
97+
record.state = 'canceled'
98+
return True
99+
100+
def action_reset(self):
101+
for record in self:
102+
if record.state not in ['sold', 'canceled']:
103+
raise UserError('Cannot reset.')
104+
record.state = 'new'
105+
return True
106+
107+
# Make sure that only one offer is accepted
108+
def set_accepted_offer(self, offer):
109+
for record in self:
110+
if offer is not None:
111+
record.state = 'offer_accepted'
112+
record.selling_price = offer.price
113+
record.buyer_id = offer.partner_id
114+
else:
115+
record.state = 'offer_received'
116+
record.selling_price = 0
117+
record.buyer_id = None
118+
for o in record.offer_ids:
119+
if o.state == 'accepted' and (offer is None or o.id != offer.id):
120+
o.action_reset(propagate=False)

estate/models/property_offer.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from dateutil.relativedelta import relativedelta
2+
3+
from odoo import models, fields, api
4+
5+
6+
class PropertyOffer(models.Model):
7+
_name = 'estate.property.offer'
8+
_description = 'Property Offer'
9+
_sql_constraints = [
10+
('check_offer_price', 'CHECK(price > 0)', 'The Offer Price must be positive.'),
11+
('check_validity', 'CHECK(validity >= 0)', 'The Offer Validity must be positive.'),
12+
]
13+
_order = 'price desc'
14+
15+
state = fields.Selection([
16+
('received', 'Received'),
17+
('accepted', 'Accepted'),
18+
('refused', 'Refused')
19+
], default='received', required=True, readonly=True)
20+
partner_id = fields.Many2one('res.partner', string='Partner', required=True)
21+
price = fields.Float()
22+
property_id = fields.Many2one('estate.property', string='Property', required=True)
23+
create_date = fields.Date(string='Create Date', default=lambda self: fields.Datetime.now())
24+
validity = fields.Integer(string='Validity (days)', default=7)
25+
deadline_date = fields.Date(string='Deadline Date', compute='_compute_deadline_date',
26+
inverse='_inverse_deadline_date')
27+
property_type_id = fields.Many2one('estate.property.type', related='property_id.property_type_id')
28+
29+
@api.depends('validity', 'deadline_date', 'create_date')
30+
def _compute_deadline_date(self):
31+
for record in self:
32+
record.deadline_date = record.create_date + relativedelta(days=record.validity)
33+
34+
@api.depends('validity', 'deadline_date', 'create_date')
35+
def _inverse_deadline_date(self):
36+
for record in self:
37+
record.validity = (record.deadline_date - record.create_date).days
38+
39+
def action_accept(self, propagate=True):
40+
for record in self:
41+
if propagate:
42+
record.property_id.set_accepted_offer(record)
43+
record.state = 'accepted'
44+
return True
45+
46+
def action_refuse(self, propagate=True):
47+
for record in self:
48+
if record.state == 'accepted' and propagate:
49+
record.property_id.set_accepted_offer(None)
50+
record.state = 'refused'
51+
return True
52+
53+
def action_reset(self, propagate=True):
54+
for record in self:
55+
if record.state == 'accepted' and propagate:
56+
record.property_id.set_accepted_offer(None)
57+
record.state = 'received'
58+
return True

estate/models/property_tag.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from odoo import models, fields
2+
3+
4+
class PropertyTag(models.Model):
5+
_name = 'estate.property.tag'
6+
_description = 'Property Tag'
7+
_sql_constraints = [
8+
('check_name_unique', 'UNIQUE(name)', 'The Tag name must be unique.')
9+
]
10+
_order = 'name asc'
11+
12+
name = fields.Char(string='Tag', required=True)
13+
color = fields.Integer(string='Color')

estate/models/property_type.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from odoo import models, fields, api
2+
3+
4+
class PropertyType(models.Model):
5+
_name = 'estate.property.type'
6+
_description = 'Property Type'
7+
_sql_constraints = [
8+
('check_name_unique', 'UNIQUE(name)', 'The type name must be unique.')
9+
]
10+
_order = 'sequence asc, name asc'
11+
12+
name = fields.Char(string='Title', required=True)
13+
sequence = fields.Integer('Sequence', default=1)
14+
property_ids = fields.One2many('estate.property', 'property_type_id')
15+
offer_ids = fields.One2many('estate.property.offer', 'property_type_id')
16+
offer_count = fields.Integer(compute='_compute_offer_count')
17+
18+
@api.depends('offer_ids')
19+
def _compute_offer_count(self):
20+
for record in self:
21+
record.offer_count = len(record.offer_ids)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
2+
access_estate_property_type,access_estate_property_type,model_estate_property_type,base.group_user,1,1,1,1
3+
access_estate_property_tag,access_estate_property_tag,model_estate_property_tag,base.group_user,1,1,1,1
4+
access_estate_property_offer,access_estate_property_offer,model_estate_property_offer,base.group_user,1,1,1,1
5+
access_estate_property,access_estate_property,model_estate_property,base.group_user,1,1,1,1

estate/static/description/icon.png

5.48 KB
Loading

estate/views/estate_menu_views.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<odoo>
3+
<menuitem id="estate_menu_root" name="Real Estate" web_icon="estate,static/description/icon.png">
4+
<menuitem id="estate_advertisements_menu" name="Advertisements">
5+
<menuitem id="estate_properties_menu" action="estate_property_action"/>
6+
</menuitem>
7+
<menuitem id="estate_settings_menu" name="Settings">
8+
<menuitem id="estate_property_type_menu" action="estate_property_type_action"/>
9+
<menuitem id="estate_property_tag_menu" action="estate_property_tag_action"/>
10+
</menuitem>
11+
</menuitem>
12+
</odoo>

0 commit comments

Comments
 (0)