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
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 == "accepted" or self.status == "refused":

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
if self.status == "accepted" or self.status == "refused":
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