Skip to content
Open
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
19 changes: 19 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "Real Estate",
"version": "1.0",
"depends": ["base"],
"author": "Odoo S.A.",
"category": "Productivity",
"description": """
Our brand new real estate app!
""",
"application": True,
"data": [
"security/ir.model.access.csv",
"views/estate_property_views.xml",
"views/estate_property_type_views.xml",
"views/estate_property_tag_views.xml",
"views/estate_menus.xml",
],
"license": "OPL-1",
}
4 changes: 4 additions & 0 deletions estate/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from . import estate_property
from . import estate_property_offer
from . import estate_property_type
from . import estate_property_tag
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 datetime import datetime
from dateutil.relativedelta import relativedelta

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


class Property(models.Model):
Comment thread
msho-odoo marked this conversation as resolved.
_name = "estate.property"
_description = "Real Estate Property"

name = fields.Char("Property Name", required=True)
description = fields.Text("Property Description")
postcode = fields.Char("Postcode")

date_availability = fields.Date("Available Date", copy=False, default=lambda _x: datetime.now() + relativedelta(months=+3))

expected_price = fields.Float("Expected Price")
selling_price = fields.Float("Selling Price", copy=False, readonly=True)
best_price = fields.Float("Best offer", compute="_compute_best_price")

bedrooms = fields.Integer("Number of Bedrooms", default=2)
living_area = fields.Integer("Living Area")
facades = fields.Integer("Number of Facades")

has_garage = fields.Boolean("Has a Garage")
has_garden = fields.Boolean("Has a Garden")
garden_area = fields.Integer("Garden Area")
garden_orientation = fields.Selection(
string="Garden Orientation",
selection=[
("north", "North"),
("south", "South"),
("east", "East"),
("west", "West"),
],
)
Comment thread
msho-odoo marked this conversation as resolved.

state = fields.Selection(
string="Property State",
selection=[
("new", "New"),
("offer_received", "Offer Received"),
("offer_accepted", "Offer Accepted"),
("sold", "Sold"),
("cancelled", "Cancelled"),
],
default="new",
Comment thread
msho-odoo marked this conversation as resolved.
)

total_area = fields.Integer("Total Area", compute="_compute_total_area")

property_type_id = fields.Many2one("estate.property.type", string="Property Type")

buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False)
seller_id = fields.Many2one("res.users", string="Seller", default=lambda self: self.env.user)

tags_ids = fields.Many2many("estate.property.tag", string="Property Tags")

offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers")

active = fields.Boolean(default=True)

_check_expected_price = models.Constraint("CHECK(expected_price > 0)", "Expected price must be greater than zero")
_check_selling_price = models.Constraint("CHECK(selling_price >= 0)", "Selling price must be greater or equal to zero")

@api.constrains("expected_price", "selling_price")
def _check_selling_price_proportion(self):
for record in self:
if record.selling_price == 0:
continue # 0 means no offer accepted

if float_compare(record.selling_price, 0.9 * record.expected_price, 2) < 0:
raise ValidationError(message="Selling price must be at least 90 percent of expected price")

def action_mark_as_sold(self):
for record in self:
if record.state == "cancelled":
raise UserError(message="Can't sell a cancelled auction.")

record.state = "sold"

return True

def action_cancel_selling(self):
for record in self:
if record.state == "sold":
raise UserError(message="Can't cancel a sold auction.")

record.state = "cancelled"

return True

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

@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.onchange("has_garden")
def _onchange_has_garden(self):
if self.has_garden:
self.garden_area = 10
self.garden_orientation = "north"
return

self.garden_area = 0
self.garden_orientation = ""
68 changes: 68 additions & 0 deletions estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from datetime import datetime, timedelta

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


class PropertyOffer(models.Model):
_name = "estate.property.offer"
_description = "Offer on a Real Estate Property"

price = fields.Float("Price")
status = fields.Selection(
string="Status",
copy=False,
selection=[
("accepted", "Accepted"),
("refused", "Refused"),
],
)

validity = fields.Integer("Validity (in days)", default=7)

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

partner_id = fields.Many2one("res.partner", string="Made by", required=True)
property_id = fields.Many2one("estate.property", string="Property", required=True)

_check_price = models.Constraint("CHECK(price > 0)", "Offered price must be greater than zero")

def action_accept_offer(self):
for record in self:
linked_property = record.property_id
states = linked_property.offer_ids.mapped("status")

if "accepted" in states:
raise UserError(message="An offer was already accepted")

if linked_property.state == "cancelled":
raise UserError(message="Can't accept offers on cancelled auctions")

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

return True

def action_reject_offer(self):
for record in self:
record.status = "refused"

return True

@api.depends("validity")
def _compute_date_deadline(self):
for record in self:
offer_date = record.create_date
if not offer_date:
offer_date = datetime.now()

record.date_deadline = offer_date.date() + timedelta(days=record.validity)

def _inverse_date_deadline(self):
for record in self:
offer_date = record.create_date
if not offer_date:
offer_date = datetime.now()
record.validity = (record.date_deadline - offer_date.date()).days
10 changes: 10 additions & 0 deletions estate/models/estate_property_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from odoo import fields, models


class PropertyTag(models.Model):
_name = "estate.property.tag"
_description = "Real Estate Property Tag"

name = fields.Char("Tag", required=True)

_check_name = models.Constraint("UNIQUE(name)", "Tag already exists")
8 changes: 8 additions & 0 deletions estate/models/estate_property_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from odoo import fields, models


class PropertyType(models.Model):
_name = "estate.property.type"
_description = "Real Estate Property Type"

name = fields.Char(string="Type", required=True)
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_user,access_estate_property_user,model_estate_property,base.group_user,1,1,1,1
access_estate_property_type_user,access_estate_property_type_user,model_estate_property_type,base.group_user,1,1,1,1
access_estate_property_tag_user,access_estate_property_tag_user,model_estate_property_tag,base.group_user,1,1,1,1
access_estate_property_offer_user,access_estate_property_offer_user,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_main_menu" name="Real_Estate">
<menuitem id="estate_menu_open_advertisements" name="Advertisements">
<menuitem id="estate_property_trigger_action" action="estate_property_action"/>
</menuitem>
<menuitem id="estate_menu_open_settings" name="Settings">
<menuitem id="estate_app_property_type_trigger_action" action="estate_property_type_action"/>
<menuitem id="estate_app_property_tag_trigger_action" action="estate_property_tag_action"/>
Comment on lines +8 to +9

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

oh don't make two menuitems have the same id

</menuitem>
</menuitem>
</odoo>
8 changes: 8 additions & 0 deletions estate/views/estate_property_tag_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0"?>
<odoo>
<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>
8 changes: 8 additions & 0 deletions estate/views/estate_property_type_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0"?>
<odoo>
<record id="estate_property_type_action" model="ir.actions.act_window">
<field name="name">Property Types</field>
<field name="res_model">estate.property.type</field>
<field name="view_mode">list,form</field>
</record>
</odoo>
131 changes: 131 additions & 0 deletions estate/views/estate_property_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<?xml version="1.0"?>
<odoo>

<record id="estate_property_view_search" model="ir.ui.view">
<field name="name">Property Lookup</field>
<field name="model">estate.property</field>
<field name="arch" type="xml">
<search string="Properties">
<field name="name"/>
<field name="postcode"/>
<field name="expected_price"/>
<field name="bedrooms"/>
<field name="living_area"/>
<field name="facades"/>
<separator/>
<filter name="active" string="Available" domain="[('active', '=', True)]"/>
<filter name="active" string="New" domain="[('state', 'in', ('new','offer_received'))]"/>
<group>
<filter name="postcode" string="Postcode" context="{'group_by':'postcode', 'residual_visible':True}"/>
</group>
</search>
</field>
</record>

<record id="estate_property_view_list" model="ir.ui.view">
<field name="name">Properties</field>
<field name="model">estate.property</field>
<field name="arch" type="xml">
<list string="Properties">
<field name="name"/>
<field name="postcode"/>
<field name="bedrooms"/>
<field name="living_area"/>
<field name="expected_price"/>
<field name="selling_price"/>
<field name="date_availability"/>
</list>
</field>
</record>

<record id="estate_property_view_form" model="ir.ui.view">
<field name="name">Properties</field>
<field name="model">estate.property</field>
<field name="arch" type="xml">
<form string="Property">
<header>
<button name="action_mark_as_sold" type="object" string="Sold"/>
<button name="action_cancel_selling" type="object" string="Cancel"/>
</header>
<sheet>
<h1><field name="name"/></h1>
<group>
<field name="tags_ids" nolabel="1" widget="many2many_tags"/>
</group>

<group>
<group>
<field name="state"/>
<field name="property_type_id"/>
<field name="postcode"/>
<field name="date_availability"/>
</group>
<group>
<field name="expected_price"/>
<field name="selling_price"/>
<field name="best_price"/>
</group>
</group>
<notebook>
<page string="Information">
<group>
<field name="description"/>
</group>
<group>
<field name="bedrooms"/>
</group>
<group>
<field name="living_area"/>
</group>
<group>
<field name="facades"/>
</group>
<group>
<field name="has_garage"/>
</group>
<group>
<field name="has_garden"/>
</group>
<group>
<field name="garden_area"/>
</group>
<group>
<field name="garden_orientation"/>
</group>
<group>
<field name="total_area"/>
</group>
</page>
<page string="Offers">
<field name="offer_ids" nolabel="1">
<list>
<field name="price"/>
<field name="partner_id"/>
<field name="status"/>
<field name="validity"/>
<field name="date_deadline"/>
<button name="action_accept_offer" string="Accept" type="object" icon="fa-check"/>
<button name="action_reject_offer" string="Reject" type="object" icon="fa-times"/>
</list>
</field>
</page>
<page string="Other">
<group>
<field name="buyer_id"/>
</group>
<group>
<field name="seller_id"/>
</group>
</page>
</notebook>
</sheet>
</form>
</field>
</record>

<record id="estate_property_action" model="ir.actions.act_window">
<field name="name">Properties</field>
<field name="res_model">estate.property</field>
<field name="view_mode">list,form</field>
</record>
</odoo>