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
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
17 changes: 17 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
'name': "Real Estate",
'version': '1.0',
'depends': ['base'],
'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/res_users_views.xml',
'views/estate_menus.xml',
],
'application': True,
'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_property_offer
from . import estate_property_tag
from . import estate_property_type
from . import res_users
165 changes: 165 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
from dateutil.relativedelta import relativedelta

from odoo import api, fields, models
from odoo.exceptions import UserError
from odoo.tools.float_utils import float_compare

GARDEN_ORIENTATIONS = [
('north', 'North'),
('south', 'South'),
('east', 'East'),
('west', 'West'),
]

PROPERTY_STATUS = [
('new', 'New'),
('offer received', 'Offer Received'),
('offer accepted', 'Offer Accepted'),
('sold', 'Sold'),
('cancelled', 'Cancelled'),
]


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

# === FIELDS ===#

name = fields.Char(
required=True)
description = fields.Text()
postcode = fields.Char()
date_availability = fields.Date(
copy=False,
default=lambda self: self._default_date_availability())
expected_price = fields.Float(
required=True)
selling_price = fields.Float(
copy=False,
readonly=True)
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=GARDEN_ORIENTATIONS,
)
active = fields.Boolean(
default=True)
state = fields.Selection(
copy=False,
default='new',
required=True,
selection=PROPERTY_STATUS,
)
property_type_id = fields.Many2one(
"estate.property.type",
string='Property Type')
salesperson_id = fields.Many2one(
"res.users",
string="Salesperson",
default=lambda self: self.env.user)
buyer_id = fields.Many2one(
"res.partner",
string="Buyer",
copy=False)
tag_ids = fields.Many2many(
"estate.property.tag",
string="Tags")
offer_ids = fields.One2many(
"estate.property.offer",
"property_id")
total_area = fields.Float(
compute='_compute_total_area',
string='Total Area')
best_price = fields.Float(
compute='_compute_best_price',
string='Best Offer')

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

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

# === COMPUTE METHODS ===#

# Default method to set date_availability to three months from today
def _default_date_availability(self):
return fields.Datetime.today() + relativedelta(months=3)

@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:
if record.offer_ids:
record.best_price = max(record.mapped('offer_ids.price'))
else:
record.best_price = 0.0

@api.onchange('garden')
def _onchange_garden(self):
if self.garden:
self.garden_area = 10
self.garden_orientation = 'north'
else:
self.garden_area = 0
self.garden_orientation = False

# === ACTION METHODS ===#

def action_set_sold(self):
for record in self:
if record.state == 'cancelled':
error_message = "Cancelled properties cannot be sold."
raise UserError(error_message)
record.state = 'sold'
return True

def action_set_canceled(self):
for record in self:
if record.state == 'sold':
error_message = "Sold properties cannot be cancelled."
raise UserError(error_message)
record.state = 'cancelled'
return True

# === CONSTRAINT METHODS ===#

@api.constrains('selling_price', 'expected_price')
def _check_selling_price(self):
for record in self:
if record.selling_price and record.expected_price:
if float_compare(record.selling_price,
record.expected_price * 0.9,
precision_rounding=0.01) < 0:
error_message = (
"The selling price must be at least 90% "
"of the expected price."
)
raise UserError(error_message)

# === CRUD OVERRIDES ===#

@api.ondelete(at_uninstall=False)
def _unlink_property(self):
for record in self:
if record.state not in ['new', 'cancelled']:
error_message = (
"Only properties in 'New' or 'Cancelled' state "
"can be deleted."
)
raise UserError(error_message)
101 changes: 101 additions & 0 deletions estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
from dateutil.relativedelta import relativedelta

from odoo import api, fields, models
from odoo.exceptions import UserError


OFFER_STATUS = [
('accepted', 'Accepted'),
('refused', 'Refused'),
]


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

# === FIELDS ===#

price = fields.Float()
status = fields.Selection(
selection=OFFER_STATUS,
copy=False)
partner_id = fields.Many2one(
"res.partner",
string='Partner',
required=True)
property_id = fields.Many2one(
"estate.property",
string='Property',
required=True,
ondelete='cascade')
validity = fields.Integer(
default=7,
string='Validity (days)')
date_deadline = fields.Date(
compute='_compute_date_deadline',
inverse='_inverse_date_deadline',
string='Deadline')
property_type_id = fields.Many2one(
related="property_id.property_type_id",
store=True,
)

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

# === COMPUTE METHODS ===#

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

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

# === ACTION METHODS ===#

def action_accept_offer(self):
for record in self:
record.status = 'accepted'
record.property_id.selling_price = record.price
record.property_id.buyer_id = record.partner_id
record.property_id.state = 'offer accepted'
return True

def action_reject_offer(self):
for record in self:
record.status = 'refused'
return True

# === CONSTRAINT METHODS ===#

@api.constrains('price')
def _check_price(self):
for record in self:
existing_offers = record.property_id.offer_ids.filtered(lambda o: o != record)
if existing_offers:
min_price = min(existing_offers.mapped('price'))
if record.price < min_price:
error_message = "The offer price can't be lower than existing offers!"
raise UserError(error_message)

# === CRUD OVERRIDES ===#

@api.model
def create(self, vals):
offer = super().create(vals)
offer.property_id.state = 'offer received'
return offer
18 changes: 18 additions & 0 deletions estate/models/estate_property_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from odoo import fields, models


class PropertyTag(models.Model):
_name = "estate.property.tag"
_description = "An estate property tag model"
_order = "name asc"

# === FIELDS ===#

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

_check_name = models.Constraint(
'unique(name)',
'The tag name must be unique!',
)
38 changes: 38 additions & 0 deletions estate/models/estate_property_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from odoo import api, fields, models


class PropertyType(models.Model):
_name = "estate.property.type"
_description = "An estate property type model"
_order = "name asc"

# === FIELDS ===#

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

_check_name = models.Constraint(
'unique(name)',
'The property type name must be unique!',
)

# === COMPUTE METHODS ===#

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


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

property_ids = fields.One2many(
"estate.property",
"salesperson_id",
domain=['|', ('state', '=', 'new'), ('state', '=', '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
access_estate_property,estate.property,model_estate_property,base.group_user,1,1,1,1
access_estate_property_type,estate.property.type,model_estate_property_type,base.group_user,1,1,1,1
access_estate_property_tag,estate.property.tag,model_estate_property_tag,base.group_user,1,1,1,1
access_estate_property_offer,estate.property.offer,model_estate_property_offer,base.group_user,1,1,1,1
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_menu_root" name="Real Estate">
<menuitem id="estate_menu_advertisements" name="Advertisements">
<menuitem id="estate_menu_properties" action="estate_property_action"/>
</menuitem>
<menuitem id="estate_menu_settings" name="Settings">
<menuitem id="estate_menu_property_types" action="estate_property_type_action"/>
<menuitem id="estate_menu_property_tags" action="estate_property_tag_action"/>
</menuitem>
</menuitem>
</odoo>
Loading