Skip to content
Empty file added FETCH_HEAD
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why is this here? 👀

Empty file.
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
83 changes: 83 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from odoo import exceptions, api, fields, models
from odoo.tools.float_utils import float_compare


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

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')])
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I know we're not paid by line count, but it is sometimes worth using linebreaks for readability

Suggested change
garden_orientation = fields.Selection(selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')])
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')
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same here

Suggested change
state = fields.Selection(selection=[('new', 'New'), ('offer_received', 'Offer Received'), ('offer_accepted', 'Offer Accepted'), ('sold', 'Sold'), ('cancelled', 'Cancelled')], required=True, copy=False, default='new')
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", string="property Type")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

When the variable name is that explicit, you don't have to specify a string 😉

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

_check_expected_price = models.Constraint(
'CHECK(expected_price > 0)',
'A property expected price must be strictly positive',
)
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 is off here

Suggested change
_check_expected_price = models.Constraint(
'CHECK(expected_price > 0)',
'A property expected price must be strictly positive',
)
_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',
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Constraints should be after computes (see doc)


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

👌


@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.state = 'cancelled'
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

In actions, we use write to change values in records. This way, all values are changed at once and it is easier for later commits to add new changes to the action (they can simply add a new line to the dictionary).

Suggested change
property.state = 'cancelled'
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.state = 'sold'
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same here

return True

@api.constrains('selling_price')
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

You should specify all of the fields used for enforcing the constraint

Suggested change
@api.constrains('selling_price')
@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.")
46 changes: 46 additions & 0 deletions estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from odoo import exceptions, api, fields, models


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

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

_check_price = models.Constraint(
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Again, order of attributes

'CHECK(price > 0)',
'An offer price must be strictly positive',
)
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


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

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 accept_offer(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.status = 'accepted'
offer.property_id.selling_price = offer.price
offer.property_id.buyer_id = offer.partner_id
offer.property_id.state = 'offer_accepted'
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Again, try to use write

Suggested change
offer.status = 'accepted'
offer.property_id.selling_price = offer.price
offer.property_id.buyer_id = offer.partner_id
offer.property_id.state = 'offer_accepted'
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.status = 'refused'
offer.property_id.selling_price = offer.price
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same comment here

return True
13 changes: 13 additions & 0 deletions estate/models/estate_property_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from odoo import fields, models


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

name = fields.Char(required=True)

_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

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


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

name = fields.Char(required=True)

_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="property_menu_root" name="Real Estate">
<menuitem id="property_first_level_menu" name="Advertisements">
<menuitem id="property_model_menu_action" action="property_model_action"/>
</menuitem>
<menuitem id="settings_first_level_menu" name="Settings">
<menuitem id="property_type_model_menu_action" action="property_type_model_action"/>
<menuitem id="property_tag_model_menu_action" action="property_tag_model_action"/>
</menuitem>
</menuitem>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

There are specific guidelines for xml (see doc)

Suggested change
<menuitem id="property_menu_root" name="Real Estate">
<menuitem id="property_first_level_menu" name="Advertisements">
<menuitem id="property_model_menu_action" action="property_model_action"/>
</menuitem>
<menuitem id="settings_first_level_menu" name="Settings">
<menuitem id="property_type_model_menu_action" action="property_type_model_action"/>
<menuitem id="property_tag_model_menu_action" action="property_tag_model_action"/>
</menuitem>
</menuitem>
<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>

The idea is to match model names.


</odoo>
Loading