Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
6e4093e
[ADD] estate: Init and manifest
wimar-odoo Oct 22, 2025
66a0801
[IMP] estate: Models and Basic Fields
wimar-odoo Oct 22, 2025
82e151b
[IMP] estate: Security - A Brief Introduction
wimar-odoo Oct 22, 2025
cd40928
[IMP] estate: Finally, Some UI to Play With
wimar-odoo Oct 23, 2025
9a9a4f3
[IMP] estate: Basic Views
wimar-odoo Oct 23, 2025
9a90bd7
[FIX] estate: Remove useless string parameter from model attributes
wimar-odoo Oct 23, 2025
db9ded5
[FIX] estate: adjustments after first review
wimar-odoo Oct 23, 2025
fe7da46
[IMP] estate: Chapter 7 - Types, Tags and Offers
wimar-odoo Oct 24, 2025
edaf208
[IMP] estate: Chapter 8 - Computed, Inverse, and Onchange Fields
wimar-odoo Oct 24, 2025
1d073e0
[IMP] estate: Chapter 9 - Button Actions
wimar-odoo Oct 24, 2025
4353811
[IMP] estate: Chapter 10 - SQL and Python Constraints
wimar-odoo Oct 24, 2025
cad2ef0
[IMP] estate: Chapter 11 - Sprinkles
wimar-odoo Oct 27, 2025
fb58afd
[FIX] estate: PR review adjustments
wimar-odoo Oct 27, 2025
a9e718c
[IMP] estate: Chapter 12 - Inheritance
wimar-odoo Oct 27, 2025
9081d57
[ADD] estate_account: Chapter 13 - Other modules
wimar-odoo Oct 28, 2025
f1448b2
[IMP] estate: Chapter 14 - Kanban view
wimar-odoo Oct 28, 2025
b48fa58
[IMP] awesome_owl: Chapter 1 - Intro Owl Components
wimar-odoo Oct 30, 2025
bd25287
[FIX] estate: last review adjustments
wimar-odoo Oct 30, 2025
816de58
[IMP] awesome_dashboard: Chapter 2 - Build a dashboard
wimar-odoo Oct 31, 2025
eefcaf9
[FIX] awesome_owl: Last review adjustments
wimar-odoo Nov 3, 2025
2918ab0
[FIX] awesome_dashboard: Last review adjustments
wimar-odoo Nov 3, 2025
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
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,13 @@ dmypy.json

# Pyre type checker
.pyre/

# JetBrains specific folders
.idea/
*/.idea/

# Odoo specific folders
.run
*/.run/
.odev
*/.odev/
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
22 changes: 22 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "Real Estate",
"version": "1.0",
"summary": "Manage properties and buyers.",
"license": "OEEL-1",
"depends": [
"base",
],
"author": "wimar-odoo",
"description": """
Manage efficiently real estate properties for sale and match them with potential buyers.
""",
"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_property_offer_views.xml",
"views/estate_menus.xml",
],
}
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_type
from . import estate_property_tag
from . import estate_property_offer
88 changes: 88 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
from odoo import fields, models, api
from odoo.exceptions import UserError, ValidationError
from odoo.tools import float_is_zero, float_compare


class EstateProperty(models.Model):
_name = "estate.property"
_description = "Estate Property"

name = fields.Char(string="Title", required=True)
description = fields.Text()
postcode = fields.Char()
date_availability = fields.Date(string="Available From", copy=False, default=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)")
facades = fields.Integer()
garage = fields.Boolean()
garden = fields.Boolean()
garden_area = fields.Integer(string="Total Area (sqm)")
garden_orientation = fields.Selection(
selection=[("north", "North"), ("south", "South"), ("east", "East"), ("west", "West")],
help="The orientation of the Garden",
)
active = fields.Boolean(default=True)
state = fields.Selection(
selection=[("new", "New"), ("received", "Offer Received"), ("accepted", "Offer Accepted"), ("sold", "Sold"), ("cancelled", "Cancelled")],
required=True,
copy=False,
default="new",
help="The current state of the property",
)
property_type_id = fields.Many2one("estate.property.type")
salesperson_id = fields.Many2one("res.users", string="Salesman", default=lambda self: self.env.user)
buyer_id = fields.Many2one("res.partner", copy=False)
tag_ids = fields.Many2many("estate.property.tag")
offer_ids = fields.One2many("estate.property.offer", "property_id")
total_area = fields.Integer(compute="_compute_total_area")
best_price = fields.Float(string="Best offer", compute="_compute_best_price")

@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")
def _compute_best_price(self):
for record in self:
if record.offer_ids:
record.best_price = max(self.offer_ids.mapped("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 = ""

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

Choose a reason for hiding this comment

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

It's better to make the error message translatable :)

raise UserError(_("Cancelled properties cannot be sold."))

You could also do something like this

Suggested change
for record in self:
if record.state == "cancelled":
raise UserError("Cancelled properties cannot be sold.")
record.state = "sold"
return True
if self.filtered(lambda x: x.state == "cancelled"):
raise UserError(_("Cancelled properties cannot be sold."))
self.state = "sold"
return True


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

_positive_expected_price = models.Constraint(
"CHECK(expected_price > 0)",
"The expected price must be strictly positive.",
)

Choose a reason for hiding this comment

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

just a small comment like that but nothing mandatory: I feel like it's more readable to have the constraints just under the fields, so they are not "hidden" between all those functions :)

but it's just a personal preference


@api.constrains("selling_price", "expected_price")
@api.onchange("selling_price", "expected_price")
def _check_ninety_percent(self):
for record in self:
if not float_is_zero(record.selling_price, 2) and float_compare(record.expected_price * 0.9, record.selling_price, 2) == 1:
raise ValidationError("The selling price must be at least 90% of the expected price! You must reduce the expected price if you want to accept this offer.")
51 changes: 51 additions & 0 deletions estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from odoo import models, fields, api, _
from odoo.exceptions import UserError


class EstatePropertyOffer(models.Model):
_name = "estate.property.offer"
_description = "Estate Property Offer"

price = fields.Float()
status = fields.Selection(
selection=[("accepted", "Accepted"), ("refused", "Refused")],
copy=False,
)
partner_id = fields.Many2one("res.partner", string="Buyer", required=True)

Choose a reason for hiding this comment

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

linter cries about this space 😅

Suggested change
partner_id = fields.Many2one("res.partner", string="Buyer", required=True)
partner_id = fields.Many2one("res.partner", string="Buyer", required=True)

property_id = fields.Many2one("estate.property", required=True)
validity = fields.Integer(string="Validity (days)", default=7)
date_deadline = fields.Date(string="Deadline", compute="_compute_date_deadline", inverse="_inverse_date_deadline")

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

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

def action_accept_offer(self):
for record in self:
if record.property_id.state not in ("accepted", "sold", "cancelled"):
record.property_id.state = "accepted"
record.status = "accepted"
record.property_id.selling_price = self.price
record.property_id.buyer_id = self.partner_id
else:
raise UserError(_("This offer can't be accepted because the property is currently %s.")% record.property_id.state)
return True

def action_refuse_offer(self):
for record in self:
if record.status == "accepted":
record.property_id.state = "new"
record.status = "refused"
record.property_id.selling_price = 0.0
record.property_id.buyer_id = ""
return True

_positive_offer_price = models.Constraint(
"CHECK(price > 0)",
"The offer price must be strictly positive.",
)
13 changes: 13 additions & 0 deletions estate/models/estate_property_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from odoo import models, fields


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

name = fields.Char(required=True)

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


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

name = fields.Char(required=True)

_unique_property_type_name = models.Constraint(
"UNIQUE(name)",
"The tag name must be unique.",
)
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

Choose a reason for hiding this comment

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

missing line break 😄

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" encoding="UTF-8"?>
<odoo>
<menuitem id="estate_property_root" name="Real Estate">
<menuitem id="estate_property_advertisements_menu" name="Advertisements">
<menuitem id="estate_property_menu_action" action="estate_property_action"/>
</menuitem>
<menuitem id="estate_property_settings_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>
36 changes: 36 additions & 0 deletions estate/views/estate_property_offer_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="estate_property_offer_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="New property offer">
<sheet>
<group>
<field name="price"/>
<field name="partner_id"/>
<field name="validity"/>
<field name="date_deadline"/>
<field name="status"/>
</group>
</sheet>
</form>
</field>
</record>

<record id="estate_property_offer_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">
<field name="price"/>
<field name="partner_id"/>
<field name="validity"/>
<field name="date_deadline"/>
<button name="action_accept_offer" type="object" icon="fa-check" title="Accept offer"/>
<button name="action_refuse_offer" type="object" icon="fa-times" title="Refuse offer"/>
<field name="status"/>
</list>
</field>
</record>
</odoo>
22 changes: 22 additions & 0 deletions estate/views/estate_property_tag_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<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_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 string="New property tag">
<sheet>
<h1>
<field name="name" placeholder="Property tag"/>
</h1>
</sheet>
</form>
</field>
</record>
</odoo>
22 changes: 22 additions & 0 deletions estate/views/estate_property_type_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">form</field>
</record>

<record id="estate_property_type_form" 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="New property type">
<sheet>
<h1>
<field name="name" placeholder="Property type"/>
</h1>
</sheet>
</form>
</field>
</record>
</odoo>
Loading