Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
c408da5
[ADD] Estate: init
haall-odoo Nov 18, 2025
a946449
[IMP] estate: chapter 3
haall-odoo Nov 18, 2025
6deb7a6
[FIX] estate: CI PEP8 conformity for EoF
haall-odoo Nov 18, 2025
e429205
[FIX] estate: PEP8 conformity\nestate_property.py whitespace in blank…
haall-odoo Nov 18, 2025
ffb822c
[IMP] estate: Chapter 4
haall-odoo Nov 18, 2025
c71e880
[FIX] estate: fix whitespace in blank line
haall-odoo Nov 18, 2025
e01993f
[FIX] estate: license fix
haall-odoo Nov 18, 2025
de4a315
[IMP] estate: Chapter 5
haall-odoo Nov 18, 2025
de78e31
[IMP] Estate: Chapter 5 Sec 1
haall-odoo Nov 19, 2025
adb3316
[IMP] Estate: Chapter 6
haall-odoo Nov 19, 2025
e66dd36
[FIX] estate: review 1
haall-odoo Nov 19, 2025
55064b2
[FIX] estate: review fix 2 + chapter 7 part 1
haall-odoo Nov 19, 2025
218b8f0
[IMP] estate: chapter 7
haall-odoo Nov 19, 2025
ff73e1b
[FIX] estate: PEP8 fix
haall-odoo Nov 19, 2025
871dc8d
[IMP] estate: add computed field for total_area and best_price
haall-odoo Nov 19, 2025
6407935
[IMP] estate: chapter 8 reversed field
haall-odoo Nov 19, 2025
0b791ab
[FIX] gitignore: ignore vscode config
haall-odoo Nov 20, 2025
b5fc120
[IMP] estate: chapter 8
haall-odoo Nov 20, 2025
ac39bed
[FIX] estate: estate_property.py - EOL
haall-odoo Nov 20, 2025
ee8a1be
[IMP] estate: chapter 9
haall-odoo Nov 20, 2025
0faad12
[FIX] estate: chapter 9
haall-odoo Nov 20, 2025
be0cfdf
[IMP] estate: chapter 10
haall-odoo Nov 20, 2025
75b4558
[FIX] estate: inline comments spacing in estate_property_odder.py
haall-odoo Nov 20, 2025
d76827d
[FIX] estate: PEP8 correction estate_property_tag.py
haall-odoo Nov 20, 2025
88cd366
[FIX] estate: bypass constraint modification
haall-odoo Nov 20, 2025
70eb50e
[IMP] estate: add a ondelete decorator to reset the selling price
haall-odoo Nov 20, 2025
bcbe15d
[IMP] estate: chapter 11 part 1
haall-odoo Nov 20, 2025
73aacdc
[IMP] estate: chapter 11 part 2
haall-odoo Nov 20, 2025
5f60173
[FIX] estate: whitespace in blank line in estate_property.py
haall-odoo Nov 20, 2025
cd5b754
[IMP] estate: chapter 11 color picker part
haall-odoo Nov 20, 2025
c8cb661
[IMP] estate: chapter 11 part 3
haall-odoo Nov 21, 2025
6f106c1
[FIX] estate: PEP8 fix in estate_property_offer.py
haall-odoo Nov 21, 2025
3607166
[IMP] estate: chapter 11 end
haall-odoo Nov 21, 2025
09fdf45
[FIX] estate: estate.property.offer stored -> store
haall-odoo Nov 21, 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,6 @@ dmypy.json

# Pyre type checker
.pyre/


.vscode/
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',
'version': '1.0',
'depends': ['base'],
'author': 'Odoo S.A.',
'application': True,
'installable': True,
'category': '',
'description': '',
'license': 'LGPL-3',
'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_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
108 changes: 108 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
from odoo import api, fields, models
from odoo.exceptions import UserError, ValidationError
from odoo.tools.float_utils import float_compare


class EstateProperty(models.Model):
_name = "estate.property"
_description = "Estate Property"
_order = "id desc"
_check_expected_price = models.Constraint(
Copy link
Contributor

Choose a reason for hiding this comment

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

Constraints should be just below field definition. Check the coding guidelines for the order of the methods and fields because it's not the only one misplaced :)

'CHECK(expected_price > 0)',
'The expected price should be stricly positive'
)
_check_selling_price = models.Constraint(
'CHECK(selling_price >= 0)',
'The selling price should be positive'
)

name = fields.Char(required=True)
description = fields.Text()
postcode = fields.Char()
date_availability = fields.Date(copy=False, default=fields.Date.add(fields.Date.today(), months=3), string="Available From")
expected_price = fields.Float(required=True)
selling_price = fields.Float(readonly=True, copy=False)
bedrooms = fields.Integer(default=2)
living_area = fields.Integer()
facades = fields.Integer()
garage = fields.Boolean()
garden = fields.Boolean()
garden_area = fields.Integer()
garden_orientation = fields.Selection(
string='Orientation',
selection=[
('north', 'North'),
('west', 'West'),
('south', 'South'),
('east', 'East')
],
help="Choose the appropriate orientation of the garden"
)
active = fields.Boolean(default=True)
state = fields.Selection(
string="Estate status",
selection=[('new', 'New'), ('offer_received', 'Offer Received'), ('offer_accepted', 'Offer Accepted'), ('sold', 'Sold'), ('cancelled', 'Cancelled')],
help='This field explain the estate status.',
required=True,
copy=False,
default='new'
)
property_type_id = fields.Many2one("estate.property.type", string="Type")
seller_id = fields.Many2one("res.users", string="Salesman", default=lambda self: self.env.user, domain="[('type', '=', 'internal')]")
buyer_id = fields.Many2one("res.partner", string="Buyer", domain="[('type', '=', 'portal')]")
tag_ids = fields.Many2many("estate.property.tag")
offer_ids = fields.One2many("estate.property.offer", "property_id")
total_area = fields.Float(compute="_compute_total_area", string="Total Area (sqm)")
best_price = fields.Float(compute="_compute_best_offer", string="Best Offer")

@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")
def _compute_best_offer(self):
for record in self:
record.best_price = max(record.offer_ids.mapped("price")) if len(record.offer_ids) > 0 else 0.0
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
record.best_price = max(record.offer_ids.mapped("price")) if len(record.offer_ids) > 0 else 0.0
record.best_price = max(record.offer_ids.mapped("price"), default=0.0)
  1. You never need to check the length if you want to check if a recordset is filled, just do if record.offer_ids:
  2. You can simplify a bit like this ☝️


@api.onchange("garden")
def _onchange_garden(self):
for record in self:
if not record.garden:
record.garden_area = 0
record.garden_orientation = ''
else:
record.garden_area = 10
record.garden_orientation = 'north'

def estate_property_action_sold(self):
self.__estate_property_action_sold_cancel('sold', "A cancelled property cannot be sold!", "This property is already sold!")
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
self.__estate_property_action_sold_cancel('sold', "A cancelled property cannot be sold!", "This property is already sold!")
self._estate_property_action_sold_cancel('sold', "A cancelled property cannot be sold!", "This property is already sold!")


def estate_property_action_cancel(self):
self.__estate_property_action_sold_cancel('cancelled', "A sold property cannot be cancelled!", "This property is already cancelled!")

def __estate_property_action_sold_cancel(self, target, error_message, error_same_target_message):
for record in self:
# exclude target from next condition
if record.state == target:
raise UserError(error_same_target_message)
# easiest way to exclude the other state
elif record.state in ('sold', 'cancelled'):
raise UserError(error_message)
# the property can be sold/cancelled
else:
record.state = target

@api.constrains('selling_price')
def _check_price_constraint(self):
for record in self:
if record.selling_price and float_compare(record.selling_price, record.expected_price * 0.9, precision_digits=4) < 0:
raise ValidationError("The price cannot be les than 90% of the expected price")

@api.onchange("offer_ids")
def _onchange_offer_ids(self):
for record in self:
if len(record.offer_ids) > 0:
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if len(record.offer_ids) > 0:
if record.offer_ids:

record.state = "offer_received"
else:
record.state = "new"
58 changes: 58 additions & 0 deletions estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from odoo import api, fields, models


class EstatePropertyOffer(models.Model):
_name = "estate.property.offer"
_description = "Estate Property Offer"
_order = "price desc"
_check_offer_price = models.Constraint(
'CHECK(price > 0)',
'The offer price should be stricly positive'
)

price = fields.Float()
status = fields.Selection(
string="Status",
copy=False,
selection=[
('accepted', 'Accepted'),
('refused', 'Refused')
]
)
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_date_deadline", inverse="_inverse_date_deadline")

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

@api.onchange("date_deadline")
def _inverse_date_deadline(self):
for record in self:
record.validity = (record.date_deadline - fields.Date.today()).days

def action_accept_offer(self):
for record in self:
if record.status == 'accepted':
return False
Comment on lines +40 to +41
Copy link
Contributor

Choose a reason for hiding this comment

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

It's weird to have a method that returns False in the middle of the loop.
This will lead to inconsistencies.

record.property_id.selling_price = record.price
record.property_id.buyer_id = record.partner_id
for offer in record.property_id.offer_ids:
offer.status = "refused"
record.status = 'accepted'
record.property_id.state = 'offer_accepted'
return True

def action_refuse_offer(self):
for record in self:
record.status = "refused"
return True

@api.ondelete(at_uninstall=False)
def _unlink_if_deleted(self):
for record in self:
record.property_id.selling_price = 0.0
Comment on lines +57 to +58
Copy link
Contributor

Choose a reason for hiding this comment

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

You can actually "batch write" it self.property_id.selling_price = 0.0

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 EstatePropertyTag(models.Model):
_name = "estate.property.tag"
_description = "Estate Property Tag"
_order = "name"

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


class EstatePropertyType(models.Model):
_name = "estate.property.type"
_description = "Estate Property Types"
_order = "sequence, name"

name = fields.Char(required=True)
property_ids = fields.One2many("estate.property", "property_type_id")
sequence = fields.Integer('Sequence', default=1, help="Used to order estate property types.")
offer_ids = fields.One2many("estate.property.offer", "property_type_id")
offer_count = fields.Integer(compute="_compute_offer_count")

@api.depends('offer_ids')
def _compute_offer_count(self):
for record in self:
record.offer_count = len(record.offer_ids)
6 changes: 6 additions & 0 deletions estate/security/ir.model.access.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
access_estate_model,estate_property_users,model_estate_property,base.group_user,1,1,1,1
access_estate_model_type,estate_property_type_users,model_estate_property_type,base.group_user,1,1,1,1
portal_access_estate_model,estate_property_portal,model_estate_property,base.group_portal,1,0,0,0
access_estate_model_tag,estate_property_tag_users,model_estate_property_tag,base.group_user,1,1,1,1
access_estate_model_offer,estate_property_offer_users,model_estate_property_offer,base.group_user,1,1,1,1
13 changes: 13 additions & 0 deletions estate/views/estate_menus.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0"?>
<odoo>
<menuitem id="estate_menu_root" name="Estate">
<menuitem id="estate_first_level_menu" name="Advertisements">
<menuitem id="estate_property_menu_action" action="estate_property_action"/>
</menuitem>

<menuitem id="estate_property_type_settings_menu" name="Settings">
<menuitem id="estate_property_type_menu" action="estate_property_type_action"/>
<menuitem id="estate_property_tag_menu" action="estate_property_tag_action"/>
</menuitem>
</menuitem>
</odoo>
24 changes: 24 additions & 0 deletions estate/views/estate_property_offer_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<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="Offers" editable="bottom" decoration-success="status == 'accepted'" decoration-danger="status == 'refused'">
<field name="price"/>
<field name="partner_id"/>
<field name="validity" string="Validity (days)"/>
<field name="date_deadline" string="Deadline"/>
<button type="object" name="action_accept_offer" title="accept" icon="fa-check" invisible="status in ('accepted', 'refused')"/>
<button type="object" name="action_refuse_offer" title="refuse" icon="fa-times" invisible="status in ('accepted', 'refused')"/>
</list>
</field>
</record>

<record id="estate_property_offer_action" model="ir.actions.act_window">
<field name="name">Offer</field>
<field name="res_model">estate.property.offer</field>
<field name="view_mode">list</field>
<field name="domain">[('property_type_id', '=', active_id)]</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_form_view" 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 Tag">
<sheet>
<group>
<h1><field name="name" placeholder="New property tag"/></h1>
</group>
</sheet>
</form>
</field>
</record>

<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</field>
</record>
</odoo>
53 changes: 53 additions & 0 deletions estate/views/estate_property_type_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?xml version="1.0"?>
<odoo>
<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="New Type">
<sheet>
<group class="border-bottom">
<group>
<h1><field name="name" nolabel="1" placeholder="New property type"/></h1>
</group>
<group>
<div class="d-flex flex-row-reverse">
<button class="oe_stat_button border" name="%(estate.estate_property_offer_action)d" type="action" string="Stat button" icon="fa-money">
<field name="offer_count"/>
</button>
</div>
</group>
</group>
<notebook>
<page string="Properties">
<field name="property_ids">
<list>
<field name="name"/>
<field name="expected_price"/>
<field name="state" string="Status"/>
</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>
<field name="sequence" widget="handle"/>
<field name="name" string="Type"/>
</list>
</field>
</record>

<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>
Loading