Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
b592011
[ADD] estate: Add real estate module scafolding
ollet-odoo Sep 15, 2026
9282f17
[IMP] estate: Add estate.property model
ollet-odoo Sep 15, 2026
339767a
[IMP] estate: add first access rule to estate_property
ollet-odoo Sep 15, 2026
433fc25
[IMP] estate: add Menu, form and list view
ollet-odoo Sep 15, 2026
7a55a96
[IMP] estate: Improve Property model
ollet-odoo Sep 15, 2026
703a614
[CLN] estate: Menu, form and list view
ollet-odoo Sep 15, 2026
5ec1f59
[IMP] estate: Add list view
ollet-odoo Sep 15, 2026
b365a35
[FIX] estate Property set active by default
ollet-odoo Sep 15, 2026
2dd8a20
[IMP] estate: Property do not duplicate state
ollet-odoo Sep 15, 2026
8badc9b
[IMP] estate: Property form
ollet-odoo Sep 15, 2026
18d1370
[CLN] estate: Correct review comments
ollet-odoo Sep 15, 2026
162db65
[IMP] estate: display name and state in views
ollet-odoo Sep 15, 2026
0afb8b8
[IMP] estate: add search, filter and group by on property
ollet-odoo Sep 15, 2026
7d4e7ce
[IMP] estate: add Type for properties
ollet-odoo Sep 16, 2026
a835310
[IMP] estate: add buyer and salesperson on Property
ollet-odoo Sep 16, 2026
4f8b1c9
[IMP] estate: Add Property tags
ollet-odoo Sep 16, 2026
ea8305e
[IMP] estate: Add Property Offers
ollet-odoo Sep 16, 2026
c3845d7
[IMP] estate: Add living_area on Property
ollet-odoo Sep 16, 2026
e7eabbf
[IMP] estate: Add best_price on Property
ollet-odoo Sep 16, 2026
dd0f613
[IMP] estate: add deadline and validity on property
ollet-odoo Sep 16, 2026
b24f6c7
[IMP] estate: set fields default for garden
ollet-odoo Sep 16, 2026
c8db864
[IMP] estate: add cancel and sold button on property
ollet-odoo Sep 16, 2026
774457f
[IMP] estate: ability to refuse or refuse offers
ollet-odoo Sep 16, 2026
cf79289
[IMP] estate: add data validation constraints
ollet-odoo Sep 16, 2026
a7f50eb
[CLN] estate: attributes order
ollet-odoo Sep 17, 2026
ef52fd9
[CLN] estate: use double quote everywhere
ollet-odoo Sep 17, 2026
991989f
[CLN] estate: fix field orders
ollet-odoo Sep 17, 2026
de922c4
[CLN] estate: Correct review remarks
ollet-odoo Sep 17, 2026
c0d2543
[IMP] estate: Improve user interface
ollet-odoo Sep 17, 2026
bf627c7
[IMP] estate: only allow unlinking new or cancelled properties
ollet-odoo Sep 17, 2026
9f5e770
[IMP] estate: implement chapter8 inheritance
ollet-odoo Sep 17, 2026
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
16 changes: 16 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# noinspection statement-effect
{
"name": "estate",
"depends": ["base"],
"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_property_offer_views.xml",
"views/res_users_views.xml",
"views/estate_menus.xml"
],
"author": "Odoo S.A.",
"license": "AGPL-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 property
from . import property_type
from . import property_tag
from . import property_offer
from . import res_users
113 changes: 113 additions & 0 deletions estate/models/property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
from odoo import models, fields, api
from odoo.exceptions import UserError, ValidationError
from odoo.tools.float_utils import float_compare, float_is_zero


class Property(models.Model):
_name = "estate.property"
_description = "Real estate property"
_order = "id desc"

name = fields.Char(string="Title", required=True)
description = fields.Text()
postcode = fields.Char()
date_availability = fields.Date(
string="Available From",
copy=False,
default=lambda x: fields.Date.add(fields.Date.today(), months=3)
)
expected_price = fields.Float(required=True)
selling_price = fields.Float(readonly=True, copy=False)
bedrooms = fields.Integer(default=2)
living_area = fields.Integer(string="Living Area (sqm)")
Comment thread
ollet-odoo marked this conversation as resolved.
faces = fields.Integer(string="Facades")
has_garage = fields.Boolean(string="Garage")
has_garden = fields.Boolean(string="Garden")
garden_area = fields.Integer(string="Garden Area (sqm)")
garden_orientation = fields.Selection(
selection=[
("north", "North"),
("east", "East"),
("south", "South"),
("west", "West")
]
)
active = fields.Boolean(default=True)
state = fields.Selection(
default="new",
required=True,
copy=False,
selection=[
("new", "New"),
("offer_received", "Offer Received"),
("offer_accepted", "Offer Accepted"),
("sold", "Sold"),
("cancelled", "Cancelled")
]
)
property_type_id = fields.Many2one("estate.property.type", string="Type")
Comment thread
ollet-odoo marked this conversation as resolved.
buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False)
salesperson_id = fields.Many2one("res.users", string="Salesman", default=lambda self: self.env.user)
tag_ids = fields.Many2many("estate.property.tag", string="Tags")
offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers")

total_area = fields.Float(compute="_compute_total_area")
best_price = fields.Float(string="Best Offer", compute="_compute_best_price")

# Constraints
_check_expected_price = models.Constraint(
"CHECK(expected_price > 0)",
"Expected price must be strictly positive",
)
_check_selling_price = models.Constraint(
"CHECK(selling_price >= 0)",
"Expected price must be positive",
)

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

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

@api.constrains("expected_price", "selling_price")
def _check_expected_price_selling_price(self):
for record in self:
if not float_is_zero(record.selling_price, precision_digits=2) and \
float_compare(record.expected_price * 0.90, record.selling_price, precision_digits=2) > 0:
raise ValidationError("Selling price must be above 90% of expected price")

@api.onchange("has_garden")
def _onchange_has_garden(self):
if self.has_garden:
self.garden_area = 10
self.garden_orientation = "north"
else:
self.garden_area = 0
self.garden_orientation = False

@api.ondelete(at_uninstall=False)
def prevent_unwanted_deletion(self):
if self.state not in ("new", "cancelled"):
raise UserError("Can only delete Property that are New or Cancelled")
return super().unlink()

def action_do_sold(self):
for record in self:
if record.state == "cancelled":
raise UserError("Canceled property cannot be sold")
else:
record.state = "sold"
return True

def action_do_cancel(self):
for record in self:
if record.state == "sold":
raise UserError("Sold property cannot be canceled")
else:
record.state = "cancelled"
return True
74 changes: 74 additions & 0 deletions estate/models/property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from odoo import models, fields, api
from odoo.exceptions import UserError


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

price = fields.Float()
status = fields.Selection(
copy=False,
selection=[
("accepted", "Accepted"),
("refused", "Refused"),
],
)
partner_id = fields.Many2one("res.partner", string="Partner", required=True)
property_id = fields.Many2one("estate.property", required=True)
property_id_state = fields.Selection(related="property_id.state")
validity = fields.Integer(default=7)
date_deadline = fields.Date(
compute="_compute_date_deadline", inverse="_inverse_date_deadline"
)
property_type_id = fields.Many2one(
"estate.property.type", related="property_id.property_type_id", store=True
)

_check_price = models.Constraint(
"CHECK(price > 0)",
"Offer price must be strictly positive",
)

@api.depends("validity", "create_date")
Comment thread
ollet-odoo marked this conversation as resolved.
def _compute_date_deadline(self):
for record in self:
base_date = record.create_date or fields.Date.today()
record.date_deadline = fields.Date.add(base_date, days=record.validity)

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

@api.model
def create(self, vals_list):
for vals in vals_list:
property_id = self.env['estate.property'].browse(vals['property_id'])
property_id.state = 'offer_received'
for record in property_id.offer_ids:
if record.price > vals['price']:
raise UserError("Cannot create an offer with a lower value than an existing one")
Comment on lines +54 to +56

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

It's correct of course, you can also use max() and mapped(), you will do the same thing in one line and without for loops, if you like

return super().create(vals_list)

def action_accept(self):
# TODO Investigate using write() to update records
for record in self:
if record.property_id.state in ("offer_accepted", "sold", "cancelled"):
err_msg = f"Cannot accept offer on property that is {record.property_id.state}"
raise UserError(err_msg)
record.status = "accepted"
record.property_id.buyer_id = record.partner_id
record.property_id.selling_price = record.price
record.property_id.state = "offer_accepted"
return True

def action_refuse(self):
for record in self:
record.status = "refused"
return True
15 changes: 15 additions & 0 deletions estate/models/property_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from odoo import fields, models


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

name = fields.Char(string="Name", required=True)
color = fields.Integer()

_name_uniq = models.Constraint(
"UNIQUE (name)",
"The name of the tag must be unique!",
)
22 changes: 22 additions & 0 deletions estate/models/property_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from odoo import models, fields


class PropertyType(models.Model):
_name = "estate.property.type"
_description = "Property Type"
_order = "sequence asc"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I think the type also wants order by name


name = fields.Char(string="Name", required=True)
property_ids = fields.One2many("estate.property", "property_type_id", string="Properties")
sequence = fields.Integer(default=1, help="Used to order type. Lower is better.")
offer_ids = fields.One2many("estate.property.offer", "property_type_id")
offer_count = fields.Integer(compute="_compute_offer_count")

_name_uniq = models.Constraint(
"UNIQUE (name)",
"The name of the type must be unique!",
)

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


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

property_ids = fields.One2many('estate.property', 'salesperson_id', name='Properties',
domain=[('state', 'in', ['offer_received', 'new'])])
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,model_estate_property,base.group_user,1,1,1,1
access_estate_property_type_user,access_estate_property_type,model_estate_property_type,base.group_user,1,1,1,1
access_estate_property_tag_user,access_estate_property_tag,model_estate_property_tag,base.group_user,1,1,1,1
access_estate_property_offer_user,access_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_advertisements_menu" name="Advertisements">
<menuitem id="estate_property_menu_action" action="estate_property_action"/>
</menuitem>
<menuitem id="estate_settgins_menu" name="Settings">
<menuitem id="estate_property_type_menu_action" action="estate_property_type_action"/>
<menuitem id="estate_property_tag_menu_action" action="estate_property_tag_action"/>
</menuitem>
</menuitem>
</odoo>
40 changes: 40 additions & 0 deletions estate/views/estate_property_offer_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?xml version="1.0"?>
<odoo>
<record id="estate_property_offer_view_tree" 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 editable="bottom"
decoration-success="status == 'accepted'"
decoration-danger="status == 'refused'"
>
Comment on lines +7 to +10

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

We don't like a very long line but this one is fine, it makes it easier to read the file :)

Suggested change
<list editable="bottom"
decoration-success="status == 'accepted'"
decoration-danger="status == 'refused'"
>
<list editable="bottom" decoration-success="status == 'accepted'" decoration-danger="status == 'refused'">

<field name="price"/>
<field name="partner_id"/>
<field name="validity"/>
<field name="date_deadline"/>
<button name="action_accept" title="Accept" type="object" icon="fa-check"
invisible="property_id_state in ('offer_accepted', 'sold', 'cancelled')"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

surely it's correct, but usually teams avoid adding new fields to the database unless absolutely necessary. It's no big issue here, it's a related field already but thought to mention it
For example here, if you already don't show an offer for cancelled or sold properties, you can just make the invisible condition here be something like state != False

/>
<button name="action_refuse" title="Refuse" type="object"
icon="oi-close"
invisible="property_id_state in ('offer_accepted', 'sold', 'cancelled')"
/>
</list>
</field>
</record>
<record id="estate_property_offer_view_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>
<group>
<field name="price"/>
<field name="partner_id"/>
<field name="date_deadline"/>
<field name="validity"/>
<field name="status"/>
</group>
</form>
</field>
</record>
</odoo>
46 changes: 46 additions & 0 deletions estate/views/estate_property_tag_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?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>
<record id="estate_property_tag_view_tree" 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 editable="top">
<field name="name"/>
</list>
</field>
</record>
<record id="estate_property_tag_view_form" model="ir.ui.view">
<field name="name">estate.property.tag.form</field>
<field name="model">estate.property.tag</field>
<field name="arch" type="xml">
<form>
<sheet>
<div class="oe_title">
<h1>
<group>
<field name="name"/>
</group>
</h1>
</div>
<group>
<field name="color"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="estate_property_tag_view_search" model="ir.ui.view">
<field name="name">estate.property.tag.search</field>
<field name="model">estate.property.tag</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
</search>
</field>
</record>
</odoo>
Loading