Skip to content

Commit a7e4365

Browse files
committed
[ADD] estate{,_account}: implement a module for real estate
This provides an implementation for a module allowing people to manage their properties, people to place offers and alters the users' view to include the list of properties being sold by the person
1 parent 781b590 commit a7e4365

19 files changed

+569
-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: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
'name': 'Real Estate',
3+
'depends': ['base'],
4+
'data': [
5+
'security/ir.model.access.csv',
6+
7+
'views/estate_property_tag_views.xml',
8+
'views/estate_property_offer_views.xml',
9+
'views/estate_property_type_views.xml',
10+
'views/estate_property_views.xml',
11+
'views/estate_menus.xml',
12+
'views/res_users_views.xml',
13+
],
14+
'installable': True,
15+
'application': True,
16+
'author': "Odoo",
17+
'license': 'AGPL-3'
18+
}

estate/models/__init__.py

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

estate/models/property.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
from dateutil.relativedelta import relativedelta
2+
3+
from odoo import api, fields, models
4+
from odoo.exceptions import UserError, ValidationError
5+
from odoo.tools import float_compare, float_is_zero
6+
7+
8+
class Property(models.Model):
9+
_name = 'estate.property'
10+
_description = "Estate property"
11+
_order = 'id desc'
12+
13+
name = fields.Char(string="Title", required=True)
14+
description = fields.Text()
15+
postcode = fields.Char()
16+
date_availability = fields.Date(
17+
string="Available From",
18+
default=lambda self: fields.Date.today() + relativedelta(months=3),
19+
copy=False,
20+
)
21+
expected_price = fields.Float(required=True)
22+
selling_price = fields.Float(readonly=True, copy=False)
23+
bedrooms = fields.Integer(default=2)
24+
living_area = fields.Integer(string="Living Area (sqm)")
25+
facades = fields.Integer()
26+
garage = fields.Boolean()
27+
garden = fields.Boolean()
28+
garden_area = fields.Integer(string="Garden Area (sqm)")
29+
garden_orientation = fields.Selection(
30+
selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')]
31+
)
32+
state = fields.Selection(
33+
default="new",
34+
string="Status",
35+
selection=[
36+
('new', 'New'),
37+
('offer_received', 'Offer Received'),
38+
('offer_accepted', 'Offer Accepted'),
39+
('sold', 'Sold'),
40+
('cancelled', 'Cancelled'),
41+
],
42+
required=True,
43+
copy=False,
44+
)
45+
active = fields.Boolean(default=True)
46+
property_type_id = fields.Many2one('estate.property.type', string="Property Type")
47+
partner_id = fields.Many2one('res.partner', string="Buyer", readonly=True)
48+
user_id = fields.Many2one('res.users', string="Salesman", default=lambda self: self.env.user)
49+
tag_ids = fields.Many2many('estate.property.tag', string="Property Tags")
50+
offer_ids = fields.One2many('estate.property.offer', 'property_id', string="Offers")
51+
total_area = fields.Integer(string="Total Area (sqm)", compute='_compute_total_area')
52+
best_offer = fields.Float(default=0.0, compute="_compute_best_offer")
53+
54+
_expected_price_strictly_pos = models.Constraint(
55+
"CHECK(expected_price > 0)", "The expected price must be strictly positive."
56+
)
57+
_selling_price_pos = models.Constraint(
58+
"CHECK(selling_price >= 0)", "The selling price must be positive."
59+
)
60+
61+
@api.depends('living_area', 'garden_area')
62+
def _compute_total_area(self):
63+
for record in self:
64+
record.total_area = record.living_area + record.garden_area
65+
66+
@api.depends('offer_ids')
67+
def _compute_best_offer(self):
68+
for record in self:
69+
record.best_offer = max(record.offer_ids.mapped('price')) if record.offer_ids else 0.0
70+
71+
@api.onchange('garden')
72+
def _onchange_garden(self):
73+
self.garden_area = 10 if self.garden else 0
74+
self.garden_orientation = 'north' if self.garden else ''
75+
76+
@api.constrains('selling_price', 'expected_price')
77+
def _check_selling_price(self):
78+
for record in self:
79+
if (
80+
not float_is_zero(record.selling_price, precision_digits=2) and
81+
float_compare(record.selling_price, record.expected_price * 0.9, precision_digits=2) == -1
82+
):
83+
raise ValidationError(
84+
self.env._(
85+
"The selling price must be at least 90% of the expected price! "
86+
"You must reduce the expected price if you want to accept this offer."
87+
)
88+
)
89+
90+
def action_set_sold(self):
91+
if self.state == 'cancelled':
92+
raise UserError(self.env._("Cancelled properties cannot be sold."))
93+
94+
self.state = 'sold'
95+
return True
96+
97+
def action_set_cancelled(self):
98+
if self.state == 'sold':
99+
raise UserError(self.env._("Sold properties cannot be cancelled."))
100+
101+
self.state = 'cancelled'
102+
return True
103+
104+
@api.ondelete(at_uninstall=False)
105+
def _unlink_except_new_or_cancelled(self):
106+
for record in self:
107+
if record.state not in ['new', 'cancelled']:
108+
raise UserError(
109+
self.env._("Only new and cancelled properties can be deleted.")
110+
)

estate/models/property_offer.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from dateutil.relativedelta import relativedelta
2+
3+
from odoo import api, fields, models
4+
from odoo.exceptions import UserError
5+
6+
7+
class PropertyOffer(models.Model):
8+
_name = 'estate.property.offer'
9+
_description = "Property Offer"
10+
_order = "price desc"
11+
12+
price = fields.Float()
13+
status = fields.Selection(selection=[('accepted', 'Accepted'), ('refused', 'Refused')], copy=False)
14+
partner_id = fields.Many2one('res.partner', required=True)
15+
property_id = fields.Many2one('estate.property', required=True)
16+
property_type_id = fields.Many2one(related='property_id.property_type_id', store=True)
17+
validity = fields.Integer(default=7, string="Validity (days)")
18+
deadline = fields.Date(compute='_compute_deadline', inverse='_inverse_deadline')
19+
20+
_price_pos = models.Constraint(
21+
"CHECK(price > 0)", "The offer price must be strictly positive."
22+
)
23+
24+
@api.depends('validity')
25+
def _compute_deadline(self):
26+
for record in self:
27+
create_date = record.create_date or fields.Date.today()
28+
record.deadline = create_date + relativedelta(days=record.validity)
29+
30+
def _inverse_deadline(self):
31+
for record in self:
32+
record.validity = (record.deadline - fields.Date.to_date(record.create_date)).days
33+
34+
def action_confirm(self):
35+
self.status = 'accepted'
36+
for offer in self.property_id.offer_ids:
37+
if offer.id == self.id:
38+
continue
39+
40+
offer.status = 'refused'
41+
42+
self.property_id.state = 'offer_accepted'
43+
self.property_id.partner_id = self.partner_id
44+
self.property_id.selling_price = self.price
45+
return True
46+
47+
def action_refuse(self):
48+
self.status = 'refused'
49+
return True
50+
51+
@api.model_create_multi
52+
def create(self, vals):
53+
for record in vals:
54+
property = self.env['estate.property'].browse(record['property_id'])
55+
min_price = min(property.offer_ids.mapped('price')) if property.offer_ids else 0.0
56+
if record['price'] < min_price:
57+
raise UserError(self.env._("The offer must be higher than %d.", min_price))
58+
59+
property.state = 'offer_received'
60+
61+
return super().create(vals)

estate/models/property_tag.py

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

estate/models/property_type.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from odoo import api, fields, models
2+
3+
4+
class PropertyType(models.Model):
5+
_name = 'estate.property.type'
6+
_description = "Property Type"
7+
_order = "sequence desc, name"
8+
9+
name = fields.Char(required=True)
10+
property_ids = fields.One2many('estate.property', 'property_type_id')
11+
sequence = fields.Integer(help="Used to order property types. Higher is better.")
12+
offer_ids = fields.One2many('estate.property.offer', 'property_type_id')
13+
offer_count = fields.Integer(compute='_compute_offer_count')
14+
15+
_name_uniq = models.Constraint("UNIQUE(name)", "The name must be unique.")
16+
17+
@api.depends('offer_ids')
18+
def _compute_offer_count(self):
19+
for record in self:
20+
record.offer_count = len(record.offer_ids)

estate/models/res_users.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from odoo import fields, models
2+
3+
4+
class ResUsers(models.Model):
5+
_inherit = 'res.users'
6+
7+
property_ids = fields.One2many('estate.property', 'user_id', domain="[('active', '=', 'True')]")
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,access_estate_property,model_estate_property,base.group_user,1,1,1,1
3+
access_estate_property_type,access_estate_property_type,model_estate_property_type,base.group_user,1,1,1,1
4+
access_estate_property_tag,access_estate_property_tag,model_estate_property_tag,base.group_user,1,1,1,1
5+
access_estate_property_offer,access_estate_property_offer,model_estate_property_offer,base.group_user,1,1,1,1

estate/views/estate_menus.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0"?>
2+
<odoo>
3+
<menuitem id="estate_property_menu_root" name="Real Estate">
4+
<menuitem id="estate_property_advertisements_menu" name="Advertisements">
5+
<menuitem id="estate_property_model_menu_action" action="estate_property_model_action"/>
6+
</menuitem>
7+
<menuitem id="estate_property_settings_menu" name="Settings">
8+
<menuitem id="estate_property_type_model_menu_action" action="estate_property_type_model_action"/>
9+
<menuitem id="estate_property_tag_model_menu_action" action="estate_property_tag_model_action"/>
10+
</menuitem>
11+
</menuitem>
12+
</odoo>

0 commit comments

Comments
 (0)