Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
64775b8
[FIX] awesome_dashboard: Get rid of the deprecated warning
clbr-odoo Nov 21, 2025
0751fe4
[ADD] estate: Initial setup for module
sdemeesterde Jul 20, 2026
54635bf
[IMP] estate: chapter3 of tutorial
sdemeesterde Jul 20, 2026
7e7bde7
[IMP] estate: chapter4 security
sdemeesterde Jul 22, 2026
4f4d694
[IMP] estate: chapter5 Some UI to play with
sdemeesterde Jul 22, 2026
11ba4f7
[IMP] estate: chapter6: basic views
sdemeesterde Jul 22, 2026
86b3cec
[REF] estate: refactoring of ids
sdemeesterde Jul 23, 2026
a9ac2bb
[IMP] estate: chapter7: relations between models
sdemeesterde Jul 23, 2026
f4e9564
[IMP] estate: chapter8: computed fields and onchanges
sdemeesterde Jul 23, 2026
e808683
[IMP] estate: chapter9: ready for some action
sdemeesterde Jul 24, 2026
fb54768
[IMP] estate: chapter10: constraints
sdemeesterde Jul 27, 2026
dd17c4c
[IMP] estate: chapter11: Add the sprinkles
sdemeesterde Jul 28, 2026
7c6e684
[IMP] estate: chapter12: inheritance
sdemeesterde Jul 28, 2026
3e2d94b
[ADD] estate_account: chapter13: interact with other modules
sdemeesterde Jul 29, 2026
42a79f9
[IMP] estate: chapter14: A brief history of qweb
sdemeesterde Jul 29, 2026
70e3c06
[IMP] pr review: use of fields.Date instead of datetime
sdemeesterde Jul 29, 2026
eec4571
[IMP] pr review: snake case for variables & fix logic
sdemeesterde Jul 29, 2026
7250b4b
[IMP] pr review: refactoring of names following coding guidelines
sdemeesterde Jul 29, 2026
9239866
[IMP] pr review: refactoring following coding guidelines
sdemeesterde Jul 30, 2026
bbc27cf
[IMP] cli fails: add missing fields to __manifest__ & clashing labels
sdemeesterde Jul 30, 2026
fc34842
[IMP] pr review: fix naming convention & business logic error
sdemeesterde Jul 31, 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
2 changes: 1 addition & 1 deletion awesome_dashboard/controllers/controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
logger = logging.getLogger(__name__)

class AwesomeDashboard(http.Controller):
@http.route('/awesome_dashboard/statistics', type='json', auth='user')
@http.route('/awesome_dashboard/statistics', type='jsonrpc', auth='user')
def get_statistics(self):
"""
Returns a dict of statistics about the orders:
Expand Down
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.9",
"author": "odoo",
"depends": [
"base",
],
"data": [
"views/estate_property_offer_views.xml",
"views/estate_property_type_views.xml",
"views/estate_property_tag_views.xml",
"views/estate_property_views.xml",
"views/res_users_views.xml",
"views/estate_menus.xml",
"security/ir.model.access.csv",
],
"application": True,
"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_property_offer
from . import estate_property_tag
from . import estate_property_type
from . import estate_property
from . import res_users
143 changes: 143 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
from odoo import api, exceptions, fields, models
from odoo.orm.utils import ValidationError
from odoo.tools import float_compare


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

active = fields.Boolean(default=True)
name = fields.Char("Title", required=True)
description = fields.Text("Notes")
postcode = fields.Char("Postcode", required=True)
date_availability = fields.Date(
"Availability date",
copy=False,
default=lambda _: fields.Date.add(fields.Date.today(), months=3),
)
expected_price = fields.Float("Expected Price", required=True)
selling_price = fields.Float(
"Selling price",
copy=False,
readonly=True,
)
state = fields.Selection(
[
("new", "New"),
("offer_received", "Offer Received"),
("offer_accepted", "Offer Accepted"),
("sold", "Sold"),
("cancelled", "Cancelled"),
],
default="new",
required=True,
copy=False,
)
bedrooms = fields.Integer("Bedrooms", default=2)
facades = fields.Integer("Facades")
garage = fields.Boolean("Garage")
garden = fields.Boolean("Garden")
living_area = fields.Integer("Living area (sqm)")
garden_area = fields.Integer("Garden area (sqm)")
total_area = fields.Integer("Total area (sqm)", compute="_compute_total_area")
garden_orientation = fields.Selection(
[
("north", "North"),
("south", "South"),
("east", "East"),
("west", "West"),
],
)
best_offer = fields.Float("Best Offer", compute="_compute_best_price")
buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False)
sale_rep_id = fields.Many2one(
"res.users",
string="Salesperson",
default=lambda self: self.env.user,
)
property_type_id = fields.Many2one("estate.property.type")
property_tag_ids = fields.Many2many("estate.property.tag", string="Tags")
offer_ids = fields.One2many(
"estate.property.offer",
"property_id",
)

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

_check_selling_price = models.Constraint(
"CHECK(selling_price >= 0)",
"The selling price 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("offer_ids")
def _compute_best_price(self):
for record in self:
valid_offers = record.offer_ids.filtered(lambda o: o.status != "refused")
prices = valid_offers.mapped("price")
record.best_offer = max(prices, default=0)

@api.onchange("offer_ids")
def _onchange_offer(self):
for record in self:
# in case of a delete
if len(record.offer_ids) == 0:
record.state = "new"

@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

@api.constrains("expected_price", "selling_price")
def _check_offer_acceptable_price(self):
for record in self:
if (
record.state == "offer_accepted"
and float_compare(record.selling_price, record.expected_price * 0.9, 3)
<= 0
):
raise ValidationError(
self.env._(
"The selling price should be greater than 90% of the expected price.",
),
)

@api.ondelete(at_uninstall=False)
def _unlink_except_state_is_new_or_cancelled(self):
for record in self:
if record.state in ("new", "cancelled"):
raise exceptions.UserError(
self.env._(
"Property that is either new or cancelled, can't be deleted.",
),
)

def action_sold_btn(self):
for record in self:
if record.state == "cancelled":
raise exceptions.UserError(
self.env._("Cancelled properties cannot be sold."),
)
record.state = "sold"

def action_cancelled_btn(self):
for record in self:
if record.state == "sold":
raise exceptions.UserError(
self.env._("Sold properties cannot be cancelled."),
)
record.state = "cancelled"
93 changes: 93 additions & 0 deletions estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
from odoo import api, exceptions, fields, models
from odoo.tools import float_compare


class EstatePropertyOffer(models.Model):
_name = "estate.property.offer"
_description = "Real estate property offer"
_order = "price desc"

price = fields.Float("Price", required=True)
status = fields.Selection(
[("accepted", "Accepted"), ("refused", "Refused")],
copy=False,
)
validity = fields.Integer(
"Validaty (days)",
default=7,
)
date_deadline = fields.Date(
"Deadline",
compute="_computed_date_deadline",
inverse="_inverse_validity_period",
readonly=False,
)
partner_id = fields.Many2one("res.partner", required=True)
property_id = fields.Many2one("estate.property", required=True)
property_type_id = fields.Many2one(
"estate.property.type",
related="property_id.property_type_id",
store=True,
)

_check_selling_price = models.Constraint(
"CHECK(price > 0)",
"The price should be strictly positive",
)

@api.depends("create_date", "validity")
def _computed_date_deadline(self):
for offer in self:
create_date = fields.Date.to_date(offer.create_date) or fields.Date.today()
if offer.validity:
offer.date_deadline = fields.Date.add(
create_date,
days=offer.validity,
)

def _inverse_validity_period(self):
for offer in self:
create_date = fields.Date.to_date(offer.create_date) or fields.Date.today()
offer.validity = (offer.date_deadline - create_date).days

@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if vals.get("property_id"):
property_record = self.env["estate.property"].browse(
vals["property_id"],
)
offer_price = vals.get("price", 0.0)
if (
property_record.best_offer
and float_compare(offer_price, property_record.best_offer, 2) < 0
):
raise exceptions.UserError(
self.env._(
"New offer (%(offer_price).2f) must be higher than the current best offer (%(best_offer).2f).",
offer_price=offer_price,
best_offer=property_record.best_offer,
),
)
if property_record.state == "new":
property_record.state = "offer_received"
return super().create(vals_list)

def action_accept(self):
for offer in self:
if offer.property_id.buyer_id:
raise exceptions.UserError(
self.env._("One offer has already been accepted."),
)
offer.status = "accepted"
offer.property_id.state = "offer_accepted"
offer.property_id.selling_price = offer.price
offer.property_id.buyer_id = offer.partner_id

def action_refuse(self):
for offer in self:
offer.status = "refused"
if offer.property_id.state == "offer_received" and all(
s == "refused" for s in offer.property_id.offer_ids.mapped("status")
):
offer.property_id.state = "new"
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 = "Real estate property tag"
_order = "name"

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

_unique_name = models.Constraint(
"UNIQUE(name)",
"Tage name already exists. Tag names must 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 = "Real estate property type"
_order = "sequence, name"

name = fields.Char("Property type", required=True)
sequence = fields.Integer("Sequence")
offer_count = fields.Integer("Offers", compute="_compute_offers_count")
property_list_id = fields.One2many("estate.property", "property_type_id")
offer_ids = fields.One2many("estate.property.offer", "property_type_id")

_unique_name = models.Constraint(
"UNIQUE(name)",
"Property name already exists. Property names must be unique.",
)

@api.depends("offer_ids")
def _compute_offers_count(self):
for offer in self:
offer.offer_count = len(offer.offer_ids)
18 changes: 18 additions & 0 deletions estate/models/res_users.py
Comment thread
sdemeesterde marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from odoo import fields, models


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

property_ids = fields.One2many(
comodel_name="estate.property",
inverse_name="sale_rep_id",
string="Estate Properties",
domain=[
(
"state",
"in",
["new", "offer_received"],
),
],
)
5 changes: 5 additions & 0 deletions estate/security/ir.model.access.csv
Comment thread
sdemeesterde marked this conversation as resolved.
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_properties,estate.property,model_estate_property,base.group_user,1,1,1,1
access_estate_properties_type,estate.property.type,model_estate_property_type,base.group_user,1,1,1,1
access_estate_properties_tag,estate.property_tag,model_estate_property_tag,base.group_user,1,1,1,1
access_estate_properties_offer,estate.property.offer,model_estate_property_offer,base.group_user,1,1,1,1
15 changes: 15 additions & 0 deletions estate/views/estate_menus.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<menuitem id="estate_menu_root" name="Real estate">
<menuitem id="advertisement_menu" name="Advertisement">
<menuitem id="estate_property_menu_action"
action="estate_property_action" />
</menuitem>
<menuitem id="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>
Loading