Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions estate/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

from . import models
25 changes: 25 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "Estate",
"version": "19.0.1.0.0",
"depends": [
"base",
],
"author": "Roman (rohar)",
"description": """
Real Estate Advertisement
""",
# data files always loaded at installation
"data": [
"security/ir.model.access.csv",
"views/estate_property_views.xml",
"views/estate_property_offer_views.xml",
"views/estate_property_type_views.xml",
"views/estate_property_tag_views.xml",
"views/estate_menus.xml",
"views/res_user_views.xml",
],
# data files containing optionally loaded demonstration data
"demo": [],
"application": True,
"license": "LGPL-3",
}
5 changes: 5 additions & 0 deletions estate/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from . import estate_property
from . import estate_property_type
from . import estate_property_tag
from . import estate_property_offer
from . import res_user
113 changes: 113 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
from odoo import api, fields, models
from odoo.exceptions import UserError, ValidationError
from odoo.tools.float_utils import float_compare, float_is_zero


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

name = fields.Char("Property Name", required=True)
description = fields.Text()
postcode = fields.Char()
date_availability = fields.Date(
"Available From",
default=fields.Date.add(
fields.Date.today(),
months=3,
),
copy=False,
)
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()
has_garage = fields.Boolean("Garage")
has_garden = fields.Boolean("Garden")
garden_area = fields.Integer()
garden_orientation = fields.Selection(
[
("north", "North"),
("south", "South"),
("east", "East"),
("west", "West"),
],
)
Comment thread
rohar-odoo marked this conversation as resolved.
active = fields.Boolean(default=True)
state = fields.Selection(
[
("new", "New"),
("offer_received", "Offer Received"),
("offer_accepted", "Offer Accepted"),
("sold", "Sold"),
("cancelled", "Cancelled"),
],
default="new",
)
buyer_id = fields.Many2one("res.partner", copy=False)
salesperson_id = fields.Many2one("res.users", default=lambda self: self.env.user)
type_id = fields.Many2one("estate.property.type", string="Property Type")
tag_ids = fields.Many2many("estate.property.tag")
offer_ids = fields.One2many("estate.property.offer", "property_id")
total_area = fields.Integer(compute="_compute_total_area")
best_price = fields.Float(compute="_compute_best_price")

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

@api.depends("offer_ids.price")
def _compute_best_price(self):
for record in self:
record.best_price = max(record.offer_ids.mapped("price"), default=0)

@api.onchange("has_garden")
def _onchange_has_garden(self):
if self.has_garden:
self.garden_orientation = "north"
self.garden_area = 10
else:
self.garden_orientation = None
self.garden_area = 0

def action_sold(self):
for record in self:
if record.state == "cancelled":
raise UserError("A cancelled property cannot be sold !")

record.state = "sold"
return True

def action_cancel(self):
for record in self:
if record.state == "sold":
raise UserError("A sold property cannot be cancelled !")

record.state = "cancelled"
return True

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

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

@api.constrains("selling_price", "expected_price")
def check_selling_price(self):
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

should be named '_check_selling_price'

for record in self:
if float_is_zero(record.selling_price, precision_digits=2):
return
Comment on lines +105 to +106
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 an exception for 0 ?

if float_compare(record.selling_price, 0.9 * record.expected_price, precision_digits=2) == -1:
raise ValidationError("The selling price cannot be lower than 90% of the expected price.")

@api.ondelete(at_uninstall=False)
def _unlink_if_new_or_cancelled(self):
if any(not record.state in ("new", "cancelled") for record in self):
raise UserError("Can only delete an new or cancelled property.")
Comment on lines +92 to +113
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Be careful about attribute ordering

65 changes: 65 additions & 0 deletions estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from odoo import api, fields, models
from odoo.tools.float_utils import float_compare
from odoo.exceptions import UserError, ValidationError
from dateutil.relativedelta import relativedelta


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

price = fields.Float()
status = fields.Selection(
[("accepted", "Accepted"), ("refused", "Refused")], copy=False
)
Comment thread
rohar-odoo marked this conversation as resolved.
partner_id = fields.Many2one("res.partner", required=True)
property_id = fields.Many2one("estate.property", required=True)
property_type_id = fields.Many2one(related="property_id.type_id", store=True)
validity = fields.Integer("Validity (days)", default=7)
date_deadline = fields.Date(
"Deadline", compute="_compute_date_deadline", inverse="_inverse_date_deadline"
)

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

@api.depends("validity", "create_date")
def _compute_date_deadline(self):
for record in self:
date = record.create_date if record.create_date else fields.Date.today()
record.date_deadline = date + relativedelta(days=record.validity)

@api.model
def create(self, vals_list):
property = self.env["estate.property"].browse(vals_list[0]["property_id"])
if max(property.offer_ids.mapped("price"), default=0.0) > vals_list[0]["price"]:
raise UserError("New offer must be higher than existing ones.")

property.state = "offer_received"

return super().create(vals_list)
Comment on lines +35 to +43
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

What if I create multiple records ? (multiple values in vals_list) then only the first one would be checked


def _inverse_date_deadline(self):
for record in self:
days = record.date_deadline - record.create_date.date()
record.validity = days.days

def action_accept(self):
for record in self:
property_id = record.property_id
if property_id.state in ("sold", "accepted"):
raise UserError("A sold or cancelled property can't be sold.")

if (property_id.has_garden and property_id.garden_orientation == "south" and float_compare(record.price, property_id.expected_price, precision_digits=2) == -1):
raise ValidationError("The selling price cannot be lower than 90% of the expected price.")
Comment on lines +56 to +57
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This should be in a check


record.status = "accepted"
property_id.state = "sold"
property_id.buyer_id = record.partner_id
property_id.selling_price = record.price

def action_refuse(self):
self.status = "refused"
Comment on lines +45 to +65
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Attribute ordering :)

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 EstatePropertyTag(models.Model):
_name = "estate.property.tag"
_description = "Property tag"
_order = "name"

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

_check_name = models.Constraint(
"UNIQUE (name)",
"Each tag name must be unique",
)
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 EstatePropertyType(models.Model):
_name = "estate.property.type"
_description = "Property type"
_order = "sequence desc, name"

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

_check_name = models.Constraint(
"UNIQUE (name)",
"Each type name must be unique",
)

@api.depends("offer_ids")
def _compute_count(self):
for record in self:
record.count = len(record.offer_ids)
12 changes: 12 additions & 0 deletions estate/models/res_user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from odoo import fields, models


class ResUsers(models.Model):
_name = "res.users"
_inherit = ["res.users"]

property_ids = fields.One2many(
"estate.property",
"salesperson_id",
domain=[("state", "in", ("new", "offer_received"))],
)
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
estate_property_access_user,estate.property.user,model_estate_property,base.group_user,1,1,1,1
estate_property_type_access_user,estate.property.type.user,model_estate_property_type,base.group_user,1,1,1,1
estate_property_tag_access_user,estate.property.tag.user,model_estate_property_tag,base.group_user,1,1,1,1
estate_property_offer_acces_user,estate.property.offer.user,model_estate_property_offer,base.group_user,1,1,1,1
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

typo

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
44 changes: 44 additions & 0 deletions estate/tests/test_estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from odoo.exceptions import ValidationError
from odoo.tests import TransactionCase
from odoo import Command


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.has_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.action_accept()
12 changes: 12 additions & 0 deletions estate/views/estate_menus.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0"?>
<odoo>
<menuitem id="estate_property_root" name="Real Estate">
<menuitem id="estate_property_menu_advertisement" name="Advertisement">
<menuitem id="estate_property_menu" action="estate_property_action"/>
</menuitem>
<menuitem id="estate_property_menu_settings" name="Settings">
<menuitem id="estate_property_type_menu" action="estate_property_type_action"/>
<menuitem id="estate_property_tag_menu" action="estate_property_tag_action"/>
</menuitem>
</menuitem>
</odoo>
24 changes: 24 additions & 0 deletions estate/views/estate_property_offer_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0"?>
<odoo>
<record id="estate_property_offer_view_list" model="ir.ui.view">
<field name="name">estate.property.offer.list</field>
<field name="model">estate.property.offer</field>
<field name="arch" type="xml">
<list string="Offers">
<field name="price"/>
<field name="status"/>
<field name="partner_id"/>
<field name="property_id"/>
<field name="validity" optional="true"/>
<field name="date_deadline" optional="true"/>
<field name="property_type_id" optional="true"/>
Comment on lines +12 to +14
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Optional values are hide or show

</list>
</field>
</record>
<record id="estate_property_offer_action" model="ir.actions.act_window">
<field name="name">Property Offers</field>
<field name="res_model">estate.property.offer</field>
<field name="domain">[('property_type_id', '=', active_id)]</field>
<field name="view_mode">list,form</field>
</record>
</odoo>
17 changes: 17 additions & 0 deletions estate/views/estate_property_tag_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0"?>
<odoo>
<record id="estate_property_tag_view_list" model="ir.ui.view">
<field name="name">estate.property.tag.list</field>
<field name="model">estate.property.tag</field>
<field name="arch" type="xml">
<list string="Tags" editable="bottom">
<field name="name"/>
</list>
</field>
</record>
<record id="estate_property_tag_action" model="ir.actions.act_window">
<field name="name">Property Tags</field>
<field name="res_model">estate.property.tag</field>
<field name="view_mode">list,form</field>
</record>
</odoo>
Loading