Skip to content
1 change: 1 addition & 0 deletions estate/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
15 changes: 15 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
'name': 'Real Estate',
'author': "Odoo",
'depends': [
'base'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is good practice to end such lines with a ,. This way, if someone later adds a line, they don't have to add the coma so the line stays unchanged, this keeps the git history cleaner!

],
'data': [
'security/ir.model.access.csv',
'views/estate_property_views.xml',
'views/estate_menus.xml',
],
'application': True,
'installable': True,
'license': 'AGPL-3',
}
4 changes: 4 additions & 0 deletions estate/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from . import estate_property
from . import estate_property_type
from . import estate_property_tag
from . import estate_property_offer
106 changes: 106 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
from odoo import exceptions, api, fields, models
from odoo.tools.float_utils import float_compare


class Property(models.Model):
_name = "estate.property"
_description = "Properties"
_order = "id desc"

name = fields.Char(required=True)
description = fields.Text()
postcode = fields.Char()
date_availability = fields.Date(copy=False, default=fields.Date.add(fields.Date.today(), months=3))
expected_price = fields.Float(required=True)
selling_price = fields.Float(readonly=True, copy=False)
bedrooms = fields.Integer(default=2)
living_area = fields.Integer()
facades = fields.Integer()
garage = fields.Boolean()
garden = fields.Boolean()
garden_area = fields.Integer()
garden_orientation = fields.Selection(
selection=[
('north', 'North'),
('south', 'South'),
('east', 'East'),
('west', 'West'),
],
)
active = fields.Boolean(default=True)
state = fields.Selection(
selection=[
('new', 'New'),
('offer_received', 'Offer Received'),
('offer_accepted', 'Offer Accepted'),
('sold', 'Sold'),
('cancelled', 'Cancelled')
],
required=True,
copy=False,
default='new',
)
property_type_id = fields.Many2one("estate.property.type")
buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False)
sales_person_id = fields.Many2one("res.users", string="Salesperson", default=lambda self: self.env.user)
tag_ids = fields.Many2many("estate.property.tag", string="Tags")
offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers")
total_area = fields.Integer(compute="_compute_living_area")
best_price = fields.Float(compute="_compute_best_price")

@api.depends("living_area", "garden_area")
def _compute_living_area(self):
for property in self:
property.total_area = property.living_area + property.garden_area

@api.depends("offer_ids.price")
def _compute_best_price(self):
for property in self:
property.best_price = max(property.mapped("offer_ids.price"), default=0.0)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👌


_check_expected_price = models.Constraint(
'CHECK(expected_price > 0)',
'A property expected price must be strictly positive',
)

_check_selling_price = models.Constraint(
'CHECK(selling_price >= 0)',
'A property selling price must be positive',
)

@api.onchange("garden")
def _onchange_garden(self):
for property in self:
if property.garden:
property.garden_area = 10
property.garden_orientation = 'north'
else:
property.garden_area = 0
property.garden_orientation = None
Comment on lines +71 to +79
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tricky (?) question time!

At first glance, this method could also be an inverse. Can you tell why it makes more sense for it to be an onchange?

Answer

inverse are triggered whenever the field is written. onchange are called by form views.

It doesn't make much sense to set arbitrary default values when programmatically updating records, so onchange is the correct approach here.


def action_cancel(self):
for property in self:
if property.state == 'sold':
raise exceptions.UserError("Cannot cancel a sold peoperty")
else:
property.write({
'state': 'cancelled',
})
return True

def action_sold(self):
for property in self:
if property.state == 'cancelled':
raise exceptions.UserError("Cannot sell a cancelled peoperty")
else:
property.write({
'state': 'sold',
})
return True

@api.constrains('selling_price', 'expected_price')
def _check_selling_price(self):
for property in self:
if property.state == 'offer_accepted':
if float_compare(property.selling_price, property.expected_price * (90 / 100), precision_rounding=0.01) < 0:
raise exceptions.ValidationError("The selling price cannot be lower than 90% of the expected price.")
59 changes: 59 additions & 0 deletions estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from odoo import exceptions, api, fields, models


class PropertyOffer(models.Model):
_name = "estate.property.offer"
_description = "Property Offers"
_order = "price desc"

price = fields.Float()
status = fields.Selection(
selection=[
('accepted', 'Accepted'),
('refused', 'Refused')
],
copy=False
)
partner_id = fields.Many2one("res.partner", string="Buyer", required=True)
property_id = fields.Many2one("estate.property", string="Property", required=True)
validity = fields.Integer(default=7)
date_deadline = fields.Date(compute="_compute_date_deadline", inverse="_inverse_date_deadline")
property_type_id = fields.Many2one("estate.property.type", related="property_id.property_type_id", store=True)

@api.depends("validity", "create_date")
def _compute_date_deadline(self):
for offer in self:
start_date = offer.create_date or fields.Date.today()
offer.date_deadline = fields.Date.add(start_date, days=offer.validity)

_check_price = models.Constraint(
'CHECK(price > 0)',
'An offer price must be strictly positive',
)

def _inverse_date_deadline(self):
for offer in self:
start_date = fields.Date.to_date(offer.create_date) or fields.Date.today()
offer.validity = (offer.date_deadline - start_date).days

def action_accept(self):
for offer in self:
if offer.property_id.garden and offer.property_id.garden_orientation == 'south' and offer.price < offer.property_id.expected_price:
raise exceptions.ValidationError("The offer price must be higher than the expected price for this property.")
else:
offer.write({
'status': 'accepted',
})
offer.property_id.write({
'selling_price': offer.price,
'buyer_id': offer.partner_id,
'state': 'offer_accepted',
})
return True

def action_refuse(self):
for offer in self:
offer.write({
'status': 'refused',
})
return True
15 changes: 15 additions & 0 deletions estate/models/estate_property_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from odoo import fields, models


class PropertyTag(models.Model):
_name = "estate.property.tag"
_description = "Property Tags"
_order = "name"

name = fields.Char(required=True)
color = fields.Integer()

_check_name = models.Constraint(
'UNIQUE(name)',
'A property tag name must be unique',
)
Comment on lines +12 to +15
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indentation

23 changes: 23 additions & 0 deletions estate/models/estate_property_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from odoo import api, fields, models


class PropertyType(models.Model):
_name = "estate.property.type"
_description = "Property Types"
_order = "name"

name = fields.Char(required=True)
property_ids = fields.One2many("estate.property", "property_type_id", string="Properties")
sequence = fields.Integer(default=1)
offer_ids = fields.One2many("estate.property.offer", "property_type_id")
offer_count = fields.Integer(compute="_compute_offer_count")

@api.depends("offer_ids")
def _compute_offer_count(self):
for type in self:
type.offer_count = len(type.offer_ids)

_check_name = models.Constraint(
'UNIQUE(name)',
'A property type name must be unique',
)
Comment on lines +20 to +23
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indentation

5 changes: 5 additions & 0 deletions estate/security/ir.model.access.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
access_property_model,access_property_model,model_estate_property,base.group_user,1,1,1,0
access_property_type_model,access_property_type_model,model_estate_property_type,base.group_user,1,1,1,0
access_property_tag_model,access_property_tag_model,model_estate_property_tag,base.group_user,1,1,1,0
access_property_offer_model,access_property_offer_model,model_estate_property_offer,base.group_user,1,1,1,0
1 change: 1 addition & 0 deletions estate/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import test_estate_property
45 changes: 45 additions & 0 deletions estate/tests/test_estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from odoo.exceptions import ValidationError
from odoo.tests import tagged, TransactionCase
from odoo import Command


@tagged('post_install', '-at_install')
class TestEstateProperty(TransactionCase):

@classmethod
def setUpClass(cls):
super().setUpClass()
cls.estate = cls.env['estate.property'].create({
'name': 'Super test estate',
'expected_price': 100000.0,
'state': 'new',
})
cls.test_partner = cls.env['res.partner'].create({
'name': 'Maman ours',
})

def test_estate_best_price(self):
'''
Ensure best price is correctly updated when an offer is received.
'''
self.assertEqual(self.estate.best_price, 0.0)
self.estate.offer_ids = [Command.create({
'price': 125000.0,
'partner_id': self.test_partner.id,
})]
self.assertEqual(self.estate.best_price, 125000.0)

def test_accept_offer_south_facing_garden(self):
'''
Ensure offers for estates with south-facing gardens can only be accepted if above expected
price.
'''
self.estate.expected_price = 500000
self.estate.garden = True
self.estate.garden_orientation = 'south'
self.estate.offer_ids = [Command.create({
'price': 475000.0,
'partner_id': self.test_partner.id,
})]
with self.assertRaises(ValidationError):
self.estate.offer_ids.accept_offer()
13 changes: 13 additions & 0 deletions estate/views/estate_menus.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0"?>
<odoo>
<menuitem id="estate_menu_root" name="Real Estate">
<menuitem id="estate_property_menu" name="Advertisements">
<menuitem id="estate_property_menu_action" action="estate_property_action"/>
</menuitem>
<menuitem id="estate_settings_menu" name="Settings">
<menuitem id="estate_property_type_menu_action" action="estate_property_type_action"/>
<menuitem id="estate_property_tag_menu_action" action="estate_property_tag_action"/>
</menuitem>
</menuitem>

</odoo>
Loading