Skip to content

Commit 5d1ae0f

Browse files
committed
[ADD] estate: creating estate module
Features : - Creating properties, types of properties - Handling customers offers - Sorting your properties by tags or by type - Generating invoices when properties are sold and much more...
1 parent b16e643 commit 5d1ae0f

21 files changed

+536
-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+
'category': 'Tutorials',
5+
'application': True,
6+
'data': ['data/ir.model.access.csv',
7+
'views/res_users.xml',
8+
'views/estate_property_offer_views.xml',
9+
'views/estate_property_views.xml',
10+
'views/estate_property_type_views.xml',
11+
'views/estate_property_tag_views.xml',
12+
'views/estate_menus.xml',
13+
],
14+
'demo': [
15+
"demo/demo.xml",
16+
],
17+
'license': 'AGPL-3'
18+
}

estate/data/ir.model.access.csv

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_prop,access_estate_prop,model_estate_property,base.group_user,1,1,1,1
3+
access_estate_prop_type,access_estate_prop_type,model_estate_property_type,base.group_user,1,1,1,1
4+
access_estate_prop_offer,access_estate_prop_offer,model_estate_property_offer,base.group_user,1,1,1,1
5+
access_estate_prop_tag,access_estate_prop_tag,model_estate_property_tag,base.group_user,1,1,1,1

estate/demo/demo.xml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<odoo>
3+
<data>
4+
<record id="model_estate_property_action_cancel" model="ir.actions.server">
5+
<field name="name">Mass cancel</field>
6+
<field name="model_id" ref="estate.model_estate_property"/>
7+
<field name="binding_model_id" ref="estate.model_estate_property"/>
8+
<field name="binding_view_types">list</field>
9+
<field name="state">code</field>
10+
<field name="code">action = records.action_cancel()</field>
11+
</record>
12+
</data>
13+
</odoo>

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 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

estate/models/estate_property.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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
6+
from odoo.fields import Many2many, One2many
7+
8+
from .helper import format_selection
9+
10+
11+
class EstateProperty(models.Model):
12+
_name = 'estate.property'
13+
_description = 'Estate Property modelisation'
14+
_order = 'id desc'
15+
_sql_constraints = [
16+
('expected_price_strictly_positive', 'CHECK(expected_price > 0)', 'Expected price must be stricly positive'),
17+
('selling_price_positive', 'CHECK(selling_price >= 0)', 'Selling price must be positive'),
18+
]
19+
20+
name = fields.Char(required=True, string="Name")
21+
description = fields.Text(string="Description")
22+
postcode = fields.Char(string="Postal code")
23+
date_availability = fields.Date(default=lambda x: fields.Date.today() + relativedelta(months=3), copy=False,
24+
string='Availability date')
25+
expected_price = fields.Float(required=True, string='Expected price')
26+
selling_price = fields.Float(readonly=True, copy=False, string='Selling price')
27+
bedrooms = fields.Integer(default=2, string='Bedrooms')
28+
living_area = fields.Integer(string='Living area')
29+
facades = fields.Integer(string='Facades')
30+
garage = fields.Boolean(string='Garage')
31+
garden = fields.Boolean(string='Garden')
32+
garden_area = fields.Integer(string='Garden area')
33+
garden_orientation = fields.Selection(string='Orientation',
34+
selection=format_selection(['north', 'south', 'east', 'west']),
35+
)
36+
active = fields.Boolean(default=True, string='Active')
37+
state = fields.Selection(string='State',
38+
selection=format_selection(
39+
['new', 'offer received', 'offer accepted', 'sold', 'canceled']),
40+
default='new')
41+
42+
property_type_id = fields.Many2one('estate.property.type', string='Property type')
43+
44+
buyer_id = fields.Many2one('res.partner', copy=False, string='Buyer')
45+
salesperson_id = fields.Many2one('res.users', default=lambda self: self.env.user, string='Salesperson')
46+
47+
tag_ids = Many2many('estate.property.tag', string='Tags')
48+
offer_ids = One2many('estate.property.offer', 'property_id', string='Offers')
49+
50+
total_area = fields.Integer(compute='_compute_total_area', string='Total area')
51+
best_price = fields.Float(compute='_compute_best_price', string='Best offer price')
52+
53+
@api.depends('living_area', 'garden_area')
54+
def _compute_total_area(self):
55+
for estate in self:
56+
estate.total_area = estate.living_area + estate.garden_area
57+
58+
@api.depends('offer_ids')
59+
def _compute_best_price(self):
60+
for estate in self:
61+
estate.best_price = max(estate.offer_ids.mapped('price'), default=0)
62+
63+
@api.onchange('garden')
64+
def _onchange_garden(self):
65+
self.garden = [0, 10][self.garden]
66+
self.garden_orientation = ['', 'north'][self.garden]
67+
68+
@api.constrains('selling_price')
69+
def _check_price_offer_reasonable(self):
70+
if float_compare(self.selling_price, 0.9 * self.expected_price, 3) <= 0:
71+
raise ValidationError('The selling price must be at least 90% of the expected price.')
72+
73+
@api.ondelete(at_uninstall=False)
74+
def _unlink_property_if_not_new_nor_canceled(self):
75+
self.ensure_one()
76+
if self.state not in ('new', 'canceled'):
77+
raise UserError('Only new and canceled properties can be deleted.')
78+
79+
def action_sold(self):
80+
self.ensure_one()
81+
if self.state == 'canceled':
82+
raise UserError('Canceled properties cannot be sold.')
83+
self.state = 'sold'
84+
return True
85+
86+
def action_cancel(self):
87+
self.ensure_one()
88+
if self.state == 'sold':
89+
raise UserError('Sold properties cannot be canceled.')
90+
self.state = 'canceled'
91+
return True
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 api, fields, models
4+
from odoo.exceptions import UserError
5+
6+
from .helper import format_selection
7+
8+
9+
class EstatePropertyOffer(models.Model):
10+
_name = 'estate.property.offer'
11+
_order = 'price desc'
12+
_description = 'Offer modelisation'
13+
_sql_constraints = [('offer_price_strictly_positive', 'CHECK(price > 0)', 'Offer price must be stricly positive')]
14+
15+
price = fields.Float(string='Price', required=True)
16+
status = fields.Selection(copy=False, selection=format_selection(['accepted', 'refused']), string='Status')
17+
partner_id = fields.Many2one('res.partner', required=True, string='Buyer')
18+
property_id = fields.Many2one('estate.property', required=True, string='Property')
19+
20+
validity = fields.Integer(default=7, string="Validity") # in days
21+
date_deadline = fields.Date(compute='_compute_deadline', inverse='_inverse_deadline', string='Deadline')
22+
23+
property_type_id = fields.Many2one(related='property_id.property_type_id', string='Property type')
24+
25+
@api.depends('validity')
26+
def _compute_deadline(self):
27+
for offer in self:
28+
offer.date_deadline = fields.Date.today() + relativedelta(days=offer.validity)
29+
30+
def _inverse_deadline(self):
31+
for offer in self:
32+
offer.validity = (offer.date_deadline - fields.Date.today()).days
33+
34+
@api.model_create_multi
35+
def create(self, vals_list):
36+
for vals in vals_list:
37+
property_id = self.env['estate.property'].browse(vals['property_id'])
38+
if any(vals['price'] < offer.price for offer in property_id.offer_ids):
39+
raise UserError('Offer price must be higher than existing offers.')
40+
property_id.state = 'offer received'
41+
return super().create(vals_list)
42+
43+
def action_accept(self):
44+
self.ensure_one()
45+
if self.property_id.state == 'sold':
46+
raise UserError('The house was already sold.')
47+
self.status = 'accepted'
48+
self.property_id.state = 'offer accepted'
49+
self.property_id.buyer_id = self.partner_id
50+
self.property_id.selling_price = self.price
51+
return True
52+
53+
def action_refuse(self):
54+
for offer in self:
55+
if offer.status == 'accepted':
56+
raise UserError('The offer was already accepted.')
57+
offer.status = 'refused'
58+
return True

estate/models/estate_property_tag.py

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

estate/models/estate_property_type.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from odoo import fields, models
2+
3+
4+
class EstatePropertyType(models.Model):
5+
_name = 'estate.property.type'
6+
_description = 'Estate Property Type modelisation'
7+
_order = 'sequence asc, name asc'
8+
_sql_constraints = [('unique_name', 'unique(name)', 'Type name must be unique')]
9+
10+
name = fields.Char(required=True, string='Name')
11+
property_ids = fields.One2many('estate.property', 'property_type_id', string='Properties')
12+
sequence = fields.Integer(default=1)
13+
14+
offer_ids = fields.One2many('estate.property.offer', 'property_type_id', string='Offers')
15+
offer_count = fields.Integer(compute='_compute_offer_count', string='Offer Count')
16+
17+
def _compute_offer_count(self):
18+
for record in self:
19+
record.offer_count = len(record.offer_ids)

estate/models/helper.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def format_selection(l: list):
2+
return [(field, field.title()) for field in l]

0 commit comments

Comments
 (0)