Skip to content
3 changes: 3 additions & 0 deletions estate/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from . import models
20 changes: 20 additions & 0 deletions estate/__manifest__.py
Comment thread
Topvennie marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

{
"name": "estate",
"application": True,
"depends": [
"base",
],
"data": [
"security/ir.model.access.csv",
"views/estate_property_views.xml",
"views/estate_offer_views.xml",
"views/estate_type_views.xml",
"views/estate_tag_views.xml",
"views/estate_menus.xml",
"views/res_user.xml",
],
"author": "Odoo S.A.",
"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_type
from . import estate_tag
from . import estate_offer
from . import res_user
101 changes: 101 additions & 0 deletions estate/models/estate_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
from odoo import api, fields, models
from odoo.exceptions import UserError, ValidationError
from odoo.tools.date_utils import add
from odoo.tools.float_utils import float_compare


class EstateOffer(models.Model):
_name = "estate.offer"
_description = "An estate offer"
_order = "price desc"

price = fields.Float()
status = fields.Selection(
[("accepted", "Accepted"), ("refused", "Refused")], copy=False
)
validity = fields.Integer(default=7)

# Foreign fields
partner_id = fields.Many2one("res.partner", required=True)
property_id = fields.Many2one("estate.property", required=True)

# Computed fields
date_deadline = fields.Date(
compute="_compute_date_deadline", inverse="_inverse_date_deadline"
)

# Related fields
property_type_id = fields.Many2one(
related="property_id.property_type_id", store=True
)

# Constraints
_check_price = models.Constraint(
"CHECK(price > 0)",
"Offer price must be stricly positive",
)

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

@api.onchange("date_deadline")
def _inverse_date_deadline(self):
for record in self:
record.validity = (record.date_deadline - fields.Date.today()).days
Comment thread
Topvennie marked this conversation as resolved.

# Actions

def action_status_accepted(self):
self.ensure_one()

if self.status in ("accepted", "refused"):
raise UserError("The state of an offer cannot be changed once it is set")

if float_compare(self.price, self.property_id.expected_price * 0.9, 2) == -1:
raise ValidationError("Offer has to be at least 90% of the expected price")

if any(
self.property_id.offer_ids.filtered(
lambda offer: offer.status == "accepted"
)
):
raise UserError("Only one offer can be accepted")

self.property_id.state = "accepted"
self.status = "accepted"

Comment thread
Topvennie marked this conversation as resolved.
for offer in self.property_id.offer_ids:
if offer.id == self.id:
continue

offer.status = "refused"

return True

def action_status_refused(self):
self.ensure_one()

if self.status in ["accepted", "refused"]:
raise UserError("The state of an offer cannot be changed once it is set")

self.status = "refused"
return True

# Overwrites

@api.model
def create(self, vals_list):
for vals in vals_list:
property = self.env["estate.property"].browse(vals["property_id"])
if property.state == "sold":
raise UserError("No offer can be made for a sold property")

if property.state == "new":
property.state = "received"

return super().create(vals_list)
155 changes: 155 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
from odoo import fields, models
from odoo.exceptions import UserError, ValidationError
from odoo.orm.models import api
from odoo.tools.date_utils import add


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

name = fields.Char(required=True)
description = fields.Text()
postcode = fields.Char()
date_availability = fields.Date(
copy=False, default=lambda _: add(fields.Date.today(), months=+3)
)
expected_price = fields.Float(required=True)
selling_price = fields.Float(
copy=False, readonly=True, compute="_compute_selling_price"
)
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(
[("north", "North"), ("south", "South"), ("east", "East"), ("west", "West")]
)
active = fields.Boolean(default=True)
state = fields.Selection(
[
("new", "New"),
("received", "Offer Received"),
("accepted", "Offer Accepted"),
("sold", "Sold"),
("cancelled", "Cancelled"),
],
required=True,
copy=False,
default="new",
)

# Foreign fields
property_type_id = fields.Many2one(
"estate.type", required=True, ondelete="restrict"
)
buyer_id = fields.Many2one(
"res.partner",
required=False,
copy=False,
ondelete="restrict",
compute="_compute_buyer_id",
)
seller_id = fields.Many2one(
"res.users",
required=True,
default=lambda self: self.env.user,
ondelete="restrict",
)
tag_ids = fields.Many2many("estate.tag")
offer_ids = fields.One2many("estate.offer", "property_id")

# Computed fields
total_area = fields.Integer(compute="_compute_total_area")
best_price = fields.Integer(compute="_compute_best_price")

# Constraints
_check_expected_price = models.Constraint(
"CHECK(expected_price > 0)",
"Expected price must be stricly positive",
)

# Computations
@api.depends("offer_ids.status")
def _compute_selling_price(self):
for record in self:
record.selling_price = 0
for offer in record.offer_ids:
if offer.status == "accepted":
record.selling_price = offer.price
break

@api.depends("offer_ids.status")
def _compute_buyer_id(self):
for record in self:
record.buyer_id = None
for offer in record.offer_ids:
if offer.status == "accepted":
record.buyer_id = offer.partner_id
break

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

@api.depends("offer_ids.price")
def _compute_best_price(self):
for record in self:
# Fallback to 0 if no records are present
record.best_price = max(record.offer_ids.mapped("price") or [0])

@api.onchange("garden")
def _onchange_garden(self):
self.ensure_one()

if self.garden:
self.garden_area = 10
self.garden_orientation = "north"
else:
self.garden_area = 0
self.garden_orientation = False

# Actions

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

record.state = "cancelled"
return True

def action_state_sold(self):
for record in self:
accepted_offer = False
for offer in record.offer_ids:
if offer.status == "accepted":
accepted_offer = True
break

if not accepted_offer:
raise UserError("A property cannot be sold if no offer is accepted")

if record.state == "cancelled":
raise UserError("A cancelled property cannot be sold")
if record.state == "sold":
raise UserError("A sold property cannot be sold again")

record.state = "sold"
return True

# Overwrites

@api.ondelete(at_uninstall=False)
def onDelete(self):
for record in self:
if record.state not in ["new", "cancelled"]:
raise ValidationError(
"Only properties in the state new or cancelled can be deleted"
)
16 changes: 16 additions & 0 deletions estate/models/estate_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from odoo import fields, models


class EstateTag(models.Model):
_name = "estate.tag"
_description = "An estate tag"
_order = "name"

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

# Constraints
_check_name = models.Constraint(
"UNIQUE(name)",
"A tag name must be unique",
)
28 changes: 28 additions & 0 deletions estate/models/estate_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from odoo import api, fields, models


class EstateType(models.Model):
_name = "estate.type"
_description = "An estate type"
_order = "sequence, name"

name = fields.Char(required=True)
sequence = fields.Integer(required=True, default=1)

# Foreign fields
property_ids = fields.One2many("estate.property", "property_type_id")
offer_ids = fields.One2many("estate.offer", "property_type_id")

# Computed fields
offer_count = fields.Integer(compute="_compute_offer_count")

# Constraints
_check_name = models.Constraint(
"UNIQUE(name)",
"A type must be unique",
)

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


class User(models.Model):
_inherit = "res.users"

# Foreign fields

property_ids = fields.One2many(
"estate.property",
"seller_id",
)

# Computed fields

available_property_ids = fields.One2many(
"estate.property", compute="_compute_filtered_properties"
)

def _compute_filtered_properties(self):
for record in self:
record.available_property_ids = record.property_ids.filtered(
lambda x: x.state not in ("sold", "cancelled")
)
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.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1
estate.access_estate_type,access_estate_type,estate.model_estate_type,base.group_user,1,1,1,1
estate.access_estate_tag,access_estate_tag,estate.model_estate_tag,base.group_user,1,1,1,1
estate.access_estate_offer,access_estate_offer,estate.model_estate_offer,base.group_user,1,1,1,1
2 changes: 2 additions & 0 deletions estate/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import test_estate_offer
from . import test_estate_property
17 changes: 17 additions & 0 deletions estate/tests/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from odoo.tests import TransactionCase


class TestEstateCommon(TransactionCase):
def setUp(self):
super().setUp()

self.property = self.env["estate.property"].create(
{
"name": "House 1",
"expected_price": 1000,
"property_type_id": self.env["estate.type"]
.create({"name": "House"})
.id,
"seller_id": self.env.user.id,
}
)
Loading