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
19 changes: 19 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
'name': 'Estate',
'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/estate_res_user_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_properties
from . import estate_property_types
from . import estate_property_tags
from . import estate_property_offers
from . import res_user
102 changes: 102 additions & 0 deletions estate/models/estate_properties.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
from odoo import api, fields, models
from odoo.exceptions import UserError, ValidationError
from odoo.tools import float_compare

DEFAULT_GARDEN_AREA = 10
DEFAULT_GARDEN_ORIENTATION = "north"


class PropertyModel(models.Model):
_name = "estate.property"
_description = "Estate Property model"
_order = "id desc"
_check_positive_expected_price = models.Constraint(
"CHECK(expected_price >= 0)",
"The expected price must be positive."
)
_check_positive_selling_price = models.Constraint(
"CHECK(selling_price >= 0)",
"The selling price must be positive"
)

name = fields.Char("Title", required=True)
description = fields.Text()
postcode = fields.Char()
date_availability = fields.Date(default=fields.Date.add(fields.Date.today(), months=3), copy=False)
expected_price = fields.Float(required=True)
best_offer = fields.Float(compute="_get_highest_price")
selling_price = fields.Float(readonly=True, copy=False)
bedrooms = fields.Integer(default=2)
living_area = fields.Integer("Living Area (sqm)")
facades = fields.Integer()
garage = fields.Boolean()
garden = fields.Boolean()
garden_area = fields.Integer("Garden Area (sqm)")
garden_orientation = fields.Selection(
selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')]
)
total_living_area = fields.Integer("Total Area (sqm)", compute="_compute_total_area")

active = fields.Boolean(default=True)
state = fields.Selection(
selection=[
("new", "New"),
("received", "Offer Received"),
("accepted", "Offer Accepted"),
("sold", "Sold"),
("cancelled", "Cancelled")
],
string="Status",
required=True,
copy=False,
default="new"
)
property_type_id = fields.Many2one("estate.property.type")
buyer = fields.Many2one("res.partner", copy=False)
salesperson = fields.Many2one("res.users", default=lambda self: self.env.user)
tag_ids = fields.Many2many("estate.property.tag")
offer_ids = fields.One2many("estate.property.offer", "property_id")

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

@api.depends("offer_ids")
def _get_highest_price(self):
for record in self:
record.best_offer = max(record.offer_ids.mapped("price")) if record.offer_ids else 0

@api.onchange("garden")
def _update_garden_area_and_orientation(self):
if self.garden:
self.garden_area = DEFAULT_GARDEN_AREA
self.garden_orientation = DEFAULT_GARDEN_ORIENTATION
else:
self.garden_area = 0
self.garden_orientation = None

def mark_as_sold(self):
self.ensure_one()
if self.state == "cancelled":
raise UserError("A cancelled property cannot be set as sold.")
self.state = "sold"
return True

def mark_as_cancelled(self):
self.ensure_one()
if self.state == "sold":
raise UserError("A sold property cannot be set as cancelled.")
self.state = "cancelled"
return True

@api.constrains("selling_price", "expected_price")
def _check_selling_price(self):
for record in self:
if record.selling_price and float_compare(record.selling_price, record.expected_price * .9, 0) == -1:
raise ValidationError(r"The selling price cannot be lower than 90% of the expected price.")

@api.ondelete(at_uninstall=False)
def _unlink_if_new_or_cancelled(self):
if any(record.state not in ('new', 'cancelled') for record in self):
raise UserError("Only 'New' and 'Cancelled' properties can be deleted.")
62 changes: 62 additions & 0 deletions estate/models/estate_property_offers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from odoo import api, fields, models
from odoo.exceptions import UserError


class PropertyOfferModel(models.Model):
_name = "estate.property.offer"
_description = "Estate Property Offer model"
_order = "price desc"

price = fields.Float()
status = fields.Selection(
selection=[
("accepted", "Accepted"),
("refused", "Refused")
],
copy=False
)
partner_id = fields.Many2one("res.partner", required=True)
property_id = fields.Many2one("estate.property", required=True)
property_type_id = fields.Many2one(related="property_id.property_type_id", store=True)
validity = fields.Integer(default=7)
date_deadline = fields.Date(compute="_compute_deadline", inverse="_inverse_deadline")
_check_price = models.Constraint(
"CHECK(price >= 0)",
"The price of the offer must be positive."
)

@api.depends("validity")
def _compute_deadline(self):
for record in self:
record.date_deadline = fields.Date.add((record.create_date or fields.Datetime.now()), days=record.validity)

def _inverse_deadline(self):
for record in self:
record.validity = (record.date_deadline - fields.Date.to_date(record.create_date)).days if record.date_deadline else record.validity

def accept_offer(self):
self.ensure_one()
self.status = "accepted"
self.property_id.selling_price = self.price
self.property_id.buyer = self.partner_id
self.property_id.state = "accepted"
self.refuse_all_other_offers()

def refuse_all_other_offers(self):
for offer in self.property_id.offer_ids:
if offer != self:
offer.status = "refused"

def refuse_offer(self):
self.ensure_one()
self.status = "refused"

@api.model
def create(self, vals_list: list[dict]):
for val in vals_list:
estate_property = self.env["estate.property"].browse(val["property_id"])
if any(offer.price > val["price"] for offer in estate_property.offer_ids):
raise UserError("Cannot create a new offer with a lower price than an existing offer.")
if estate_property.state == 'new':
estate_property.state = 'received'
return super().create(vals_list)
14 changes: 14 additions & 0 deletions estate/models/estate_property_tags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from odoo import fields, models


class PropertyTagModel(models.Model):
_name = "estate.property.tag"
_description = "Estate Property Tag model"
_order = "name"
_check_tag_uniqueness = models.Constraint(
"UNIQUE(name)",
"Each tag should have a unique name."
)

name = fields.Char(required=True)
color = fields.Integer()
22 changes: 22 additions & 0 deletions estate/models/estate_property_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from odoo import api, fields, models


class PropertyTypeModel(models.Model):
_name = "estate.property.type"
_description = "Estate Property Type model"
_order = "sequence"
_check_type_uniqueness = models.Constraint(
"UNIQUE(name)",
"Each type should have a unique name."
)

name = fields.Char(required=True)
property_ids = fields.One2many("estate.property", "property_type_id")
offer_ids = fields.One2many("estate.property.offer", "property_type_id")
offer_count = fields.Integer("Offer Count", compute="_compute_offer_count")
sequence = fields.Integer("Sequence")

@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_user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from odoo import fields, models


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

property_ids = fields.One2many(
"estate.property", "salesperson"
, domain="['|', ('state', '=', 'new'), ('state', '=', '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
estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1
estate.access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,1
estate.access_estate_property_tag,access_estate_property_tag,estate.model_estate_property_tag,base.group_user,1,1,1,1
estate.access_estate_property_offer,access_estate_property_offer,estate.model_estate_property_offer,base.group_user,1,1,1,1
11 changes: 11 additions & 0 deletions estate/views/estate_menus.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<odoo>
<menuitem id="estate_menu_root" name="Real Estate">
<menuitem id="estate_advertisements_menu" name="Advertisements">
<menuitem id="estate_model_menu_action" action="estate_property_action"/>
</menuitem>
<menuitem id="estate_settings_menu" name="Settings">
<menuitem id="estate_property_types_menu" action="estate_property_type_action"/>
<menuitem id="estate_property_tags_menu" action="estate_property_tag_action"/>
</menuitem>
</menuitem>
</odoo>
45 changes: 45 additions & 0 deletions estate/views/estate_property_offer_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<odoo>
<record id="estate_property_offer_action" 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_id', '=', active_id)]</field>
</record>

<record id="estate_property_offer_list_view" 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="Channel" editable="bottom"
decoration-danger="status == 'refused'"
decoration-success="status == 'accepted'">
<field name="price"/>
<field name="partner_id"/>
<field name="validity"/>
<field name="date_deadline" string="Deadline"/>
<button name="accept_offer" string="Accept" type="object" icon="fa-check" invisible="status in ('accepted', 'refused')"/>
<button name="refuse_offer" string="Refuse" type="object" icon="oi-close" invisible="status in ('accepted', 'refused')"/>
<field name="property_id" optional="hide"/>
<field name="status" optional="hide"/>
</list>
</field>
</record>

<record id="estate_property_offer_form_view" 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">
<sheet>
<group>
<field name="price"/>
<field name="partner_id"/>
<field name="validity"/>
<field name="date_deadline" string="Deadline"/>
<field name="status"/>
</group>
</sheet>
</form>
</field>
</record>
</odoo>
18 changes: 18 additions & 0 deletions estate/views/estate_property_tag_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<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>

<record id="estate_property_tag_list_view" model="ir.ui.view">
<field name="name">estate.property.tag.list</field>
<field name="model">estate.property.tag</field>
<field name="arch" type="xml">
<list string="Channel" editable="bottom">
<field name="name"/>
<field name="color"/>
</list>
</field>
</record>
</odoo>
48 changes: 48 additions & 0 deletions estate/views/estate_property_type_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<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>

<record id="estate_property_type_form_view" model="ir.ui.view">
<field name="name">estate.property.type.form</field>
<field name="model">estate.property.type</field>
<field name="arch" type="xml">
<form string="Property type">
<sheet>
<button name="%(estate.estate_property_offer_action)d" string="Offers" type="action" title="Offers list" invisible="not offer_count > 0"/>
<h1>
<field name="name"/>
</h1>
<group>
<field name="offer_count"/>
</group>
<notebook>
<page string="Properties">
<field name="property_ids">
<list create="false" edit="false">
<field name="name"/>
<field name="expected_price"/>
<field name="state"/>
<field name="property_type_id" optional="hide"/>
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>

<record id="estate_property_type_list_view" model="ir.ui.view">
<field name="name">estate.property.type.list</field>
<field name="model">estate.property.type</field>
<field name="arch" type="xml">
<list string="Channel">
<field name="sequence" widget="handle"/>
<field name="name"/>
</list>
</field>
</record>
</odoo>
Loading