Skip to content
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
20 changes: 20 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
'name': 'Real Estate',
'author': 'Odoo S.A.',
'license': 'LGPL-3',
"installable": True,
"application": True,
'depends': [
'base',
],
"data": [
"security/ir.model.access.csv",

"views/estate_property_views.xml",
"views/estate_property_offer_views.xml",
"views/estate_property_tag_views.xml",
"views/estate_property_type_views.xml",
"views/estate_menus.xml",
"views/estate_inherited_users_views.xml",
],
}
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_inherited_users
from . import estate_property
from . import estate_property_offer
from . import estate_property_tag
from . import estate_property_type
12 changes: 12 additions & 0 deletions estate/models/estate_inherited_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from odoo import fields, models


class EstateInheritedUsers(models.Model):
_inherit = 'res.users'

property_ids = fields.One2many(
comodel_name='estate.property',
inverse_name='salesperson',
string='Estate Properties',
domain=['|', ('state', '=', 'new'), ('state', '=', 'offer_received')],
)
108 changes: 108 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
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 = 'Estate Property'
_order = 'id desc'

active = fields.Boolean(string='Active', default=True)
bedrooms = fields.Integer(string='Bedrooms', default=2)
best_price = fields.Float(compute='_compute_best_price')
buyer = fields.Many2one(comodel_name='res.partner', readonly=True, string='Buyer', copy=False)
date_availability = fields.Date(
string='Date availability',
copy=False,
default=lambda self: fields.Date.add(fields.Date.today(), months=3),
)
description = fields.Text(string='Description')
expected_price = fields.Float(string='Expected price', required=True)
facades = fields.Integer(string='Facades')
garage = fields.Boolean(string='Garage')
garden = fields.Boolean(string='Garden')
garden_area = fields.Integer(string='Garden area')
garden_orientation = fields.Selection(
string='Garden orientation',
selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')],
)
living_area = fields.Integer(string='Living area')
name = fields.Char(string='Name', required=True, default="Unknown")
offers = fields.One2many(comodel_name='estate.property.offer', inverse_name='property_id', string='Offers')
postcode = fields.Char(string='Postcode')
property_type = fields.Many2one(comodel_name='estate.property.type', string='Property type')
salesperson = fields.Many2one(comodel_name='res.users', string='Salesperson', default=lambda self: self.env.user)
selling_price = fields.Float(string='Selling price', readonly=True, copy=False)
state = fields.Selection(
string='State',
selection=[('new', 'New'), ('offer_received', 'Offer received'), ('offer_accepted', 'Offer accepted'), ('sold', 'Sold'), ('cancelled', 'Cancelled')],
required=True,
copy=True,
default='new',
)
tags = fields.Many2many(comodel_name='estate.property.tag', string='Tags')
total_area = fields.Integer(compute='_compute_total_area')

_strictly_positive_expected_price = models.Constraint(
'CHECK(expected_price > 0)',
'The expected price of a property should be strictly positive.',
)
_positive_selling_price = models.Constraint(
'CHECK(selling_price >= 0)',
'The selling price of a property should be positive.',
)

@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('offers.price')
def _compute_best_price(self):
for record in self:
record.best_price = max(record.offers.mapped('price')) if record.offers else 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 = None

def sold_property(self):
for record in self:
if record.state == 'cancelled':
raise UserError(self.env._('A cancelled property cannot be sold.'))
record.state = 'sold'
return True

def cancel_property(self):
for record in self:
if record.state == 'sold':
raise UserError(self.env._('A sold property cannot be cancelled.'))
record.state = 'cancelled'
return True

def action_view_offers(self):
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'res_model': 'estate.property.offer',
'name': self.env._('Offers'),
'views': [[False, 'list'], [False, 'form']],
'domain': [('property_id', '=', self.id)],
}

@api.constrains('expected_price', 'selling_price')
def _check_selling_price_relation_to_expected_price(self):
for record in self:
if float_compare(record.expected_price * 0.9, record.selling_price, 2) == 1 and not float_is_zero(record.selling_price, 2):
raise ValidationError(self.env._(r'The selling price cannot be lower than 90% of the expected price.'))

@api.ondelete(at_uninstall=False)
def _unlink_if_status_is_new_or_cancelled(self):
if any(record.state not in ['new', 'cancelled'] for record in self):
raise UserError(self.env._('Can\'t delete a property that is not new or cancelled.'))
90 changes: 90 additions & 0 deletions estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
from datetime import datetime, timedelta

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


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

date_deadline = fields.Date(string='Deadline', compute='_compute_date_deadline', inverse='_inverse_date_deadline')
partner_id = fields.Many2one(comodel_name='res.partner', string='Partner', required=True)
price = fields.Float(string='Price')
property_id = fields.Many2one(comodel_name='estate.property', string='Property', required=True)
property_type = fields.Many2one(related='property_id.property_type', string='Property type')
status = fields.Selection(
string='Status',
readonly=True,
selection=[('accepted', 'Accepted'), ('refused', 'Refused')],
copy=False,
)
validity = fields.Integer(string='Validity', default=7)

_strictly_positive_price = models.Constraint(
'CHECK(price > 0)',
'The price of an offer should be strictly positive',
)

def _compute_display_name(self):
for record in self:
record.display_name = self.env._("Estate Property Offer %s", record.id)

@api.depends('create_date', 'validity')
def _compute_date_deadline(self):
for record in self:
if record.create_date:
record.date_deadline = record.create_date + timedelta(days=record.validity)
else:
record.date_deadline = datetime.now() + timedelta(days=record.validity)

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

def accept_offer(self):
for record in self:
if any(offer.status == 'accepted' for offer in record.property_id.offers):
raise UserError(self.env._('Only one offer can be accepted by property.'))
record.status = 'accepted'
record.property_id.buyer = record.partner_id
record.property_id.selling_price = record.price
record.property_id.state = 'offer_accepted'
return True

def refuse_offer(self):
for record in self:
if record.status == 'accepted':
record.property_id.buyer = None
record.property_id.selling_price = None
record.property_id.state = 'offer_received'
record.status = 'refused'
return True

def action_view_property(self):
self.ensure_one()
return {
'type': 'ir.actions.act_window',
'res_model': 'estate.property',
'name': self.env._('Properties'),
'views': [[False, 'list'], [False, 'form']],
'domain': [('id', '=', self.property_id.id)],
}

@api.constrains('price')
def _check_price(self):
for record in self:
if float_compare(record.property_id.expected_price * 0.9, record.price, 2) == 1:
raise ValidationError(self.env._(r'The price of an offer cannot be lower than 90% of the expected price of the property.'))

@api.model
def create(self, vals_list):
for vals in vals_list:
property = self.env['estate.property'].browse(vals['property_id']).with_prefetch(self.ids)
if any(offer.price > vals['price'] for offer in property.offers):
raise UserError(self.env._('An offer cannot have a lower price than an existing offer.'))
if property.state == 'new':
property.state = 'offer_received'
return super().create(vals_list)
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 = 'Estate Property Tag'
_order = 'name'

name = fields.Char(string='Name', required=True, default="Unknown")
color = fields.Integer(string='Color')

_unique_tag_name = models.Constraint(
'unique (name)',
'The name of a property tag should 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 = 'Estate Property Type'
_order = 'sequence, name'

name = fields.Char(string='Name', required=True, default="Unknown")
offers = fields.One2many(comodel_name='estate.property.offer', inverse_name='property_type', string="Offers")
offers_count = fields.Integer(string='Offers count', compute='_compute_offers_count')
properties = fields.One2many(comodel_name='estate.property', inverse_name='property_type', string='Properties')
sequence = fields.Integer(string='Sequence', default=1, help='Used to order stages. Lower is better.')

_unique_type_name = models.Constraint(
'unique (name)',
'The name of a property type should be unique.',
)

@api.depends('offers')
def _compute_offers_count(self):
for record in self:
record.offers_count = len(record.offers)
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,model_estate_property,base.group_user,1,1,1,1
estate.access.estate.property.offer,access.estate.property.offer,model_estate_property_offer,base.group_user,1,1,1,1
estate.access.estate.property.tag,access.estate.property.tag,model_estate_property_tag,base.group_user,1,1,1,1
estate.access.estate.property.type,access.estate.property.type,model_estate_property_type,base.group_user,1,1,1,1
15 changes: 15 additions & 0 deletions estate/views/estate_inherited_users_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0"?>
<odoo>
<record id="estate_view_inherited_users_form" model="ir.ui.view">
<field name="name">estate.inherited.users.form</field>
<field name="model">res.users</field>
<field name="inherit_id" ref="base.view_users_form"/>
<field name="arch" type="xml">
<xpath expr="//notebook" position="inside">
<page string="Properties">
<field name="property_ids" />
</page>
</xpath>
</field>
</record>
</odoo>
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_action_to_properties" />
</menuitem>
<menuitem id="estate_menu_settings" name="Settings">
<menuitem id="estate_menu_property_types" action="estate_action_to_property_types" />
<menuitem id="estate_menu_property_tags" action="estate_action_to_property_tags" />
</menuitem>
</menuitem>
</odoo>
53 changes: 53 additions & 0 deletions estate/views/estate_property_offer_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?xml version="1.0"?>
<odoo>
<record id="estate_action_to_property_offers" model="ir.actions.act_window">
<field name="name">Property Offers</field>
<field name="res_model">estate.property.offer</field>
<field name="view_mode">list,form</field>
<field name="domain">[('property_type', '=', active_id)]</field>
</record>

<record id="estate_view_property_offers_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="Properties" editable="top" decoration-danger="status=='refused'" decoration-success="status=='accepted'">
<field name="price" />
<field name="partner_id" />
<field name="property_id" />
<field name="validity" />
<field name="date_deadline" />
<button name="accept_offer" type="object" string="Accept" icon="fa-check" invisible="status in ['accepted', 'refused']" />
<button name="refuse_offer" type="object" string="Refuse" icon="fa-times" invisible="status in ['accepted', 'refused']" />
</list>
</field>
</record>

<record id="estate_view_property_offers_form" model="ir.ui.view">
<field name="name">estate.property.offer.form</field>
<field name="model">estate.property.offer</field>
<field name="arch" type="xml">
<form string="Property Offer">
<header>
<button name="accept_offer" type="object" string="Accept" />
<button name="refuse_offer" type="object" string="Refuse" />
</header>
<sheet>
<div name="button_box" class="oe_button_box">
<button name="action_view_property" type="object" class="oe_stat_button" icon="fa-home">
<field name="property_id" widget="statinfo" string="Property" />
</button>
</div>
<group>
<field name="price" />
<field name="status" />
<field name="partner_id" />
<field name="property_id" />
<field name="validity" />
<field name="date_deadline" />
</group>
</sheet>
</form>
</field>
</record>
</odoo>
Loading