Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
ff5d6b3
[ADD] real estate module
waterflow80 Jan 19, 2026
c2f73b8
[IMP] Setup ruff & remove pylint
waterflow80 Jan 20, 2026
f56cb74
[ADD] security configuration for estate
waterflow80 Jan 20, 2026
a2089f1
[ADD] Add action to estate module
waterflow80 Jan 20, 2026
3f06a7a
[IMP] Added ui views
waterflow80 Jan 20, 2026
72a1ba1
[IMP] estate: add basic views the estate model
waterflow80 Jan 21, 2026
950727c
[IMP] estate: add relations between models
waterflow80 Jan 22, 2026
d1f1fad
[CLN] estate: remove `ruff.toml` and add it to `.gitignore`
waterflow80 Jan 22, 2026
7c86e25
[IMP] estate: add computed fields and onchanges
waterflow80 Jan 22, 2026
d82e9b7
[LINT] estate: add missing line before class definition
waterflow80 Jan 23, 2026
6754e8f
[LINT] estate: remove unnecessary blank line
waterflow80 Jan 23, 2026
0bda7e7
[IMP] estate: add actions to the property form
waterflow80 Jan 23, 2026
8eed366
[IMP] estate: add constraints to specific fields
waterflow80 Jan 25, 2026
5cb4dde
[FIX] estate: fix ci/tutorial error
waterflow80 Jan 25, 2026
3d8ce85
[LINT] estate: fix linting errors
waterflow80 Jan 25, 2026
04d4e09
[IMP] estate: add sprinkles
waterflow80 Jan 27, 2026
6cab90c
[CLN] estate: remove unncessary comments
waterflow80 Jan 27, 2026
52b6bce
[IMP] estate: add inheritance between views
waterflow80 Jan 27, 2026
8007f75
[IMP] estate: add create condition on offer based on price
waterflow80 Jan 28, 2026
50857f4
[FIX] estate: fix ci/tutorial error
waterflow80 Jan 28, 2026
0cd359d
[IMP] estate: add inheritance and interaction with modules
waterflow80 Jan 28, 2026
e6a4009
[FIX] estate: fix ci/tuorial error
waterflow80 Jan 28, 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
13 changes: 13 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,16 @@ dmypy.json

# Pyre type checker
.pyre/

# Ruff linter
ruff.toml

# Pre-commit config files
.pre-commit-config.yaml
.pre-commit-hooks.yaml

# Checkstyle file
check-style

# VS Code Config
.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
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.9',
'category': 'Real Estates',
'summary': 'Manage real estate operations',
'author': 'Haroune Hassine',
'license': 'LGPL-3',
'depends': [
'base_setup',
],
'application': True,
'installable': True,
'data': [
'security/ir.model.access.csv',
'views/estate_property_views.xml',
'views/estate_property_offer_views.xml',
'views/estate_property_tag_views.xml',
'views/estate_property_type_views.xml',
'views/estate_menus.xml',
'views/res_users_views.xml',
],
}
7 changes: 7 additions & 0 deletions estate/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from . import (
estate_property,
estate_property_offer,
estate_property_tag,
estate_property_type,
res_users,
)
120 changes: 120 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
from dateutil.relativedelta import relativedelta

from odoo import api, fields, models
from odoo.exceptions import UserError, ValidationError
from odoo.tools.float_utils import float_compare
from odoo.tools.translate import _

DEFAULT_GARDEN_AREA = 10


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

active = fields.Boolean('Active', default=True)
name = fields.Char('Real Estate Name', required=True)
description = fields.Text('Description')
postcode = fields.Char('Postcode')
date_availability = fields.Date('Availability Date', copy=False, default=fields.Datetime.today() + relativedelta(months=+3))
expected_price = fields.Float('Expected Price', required=True)
selling_price = fields.Float('Selling Price', readonly=True, copy=False)
bedrooms = fields.Integer('Bedrooms', default=2)
living_area = fields.Integer('Living Area (sqm)')
facades = fields.Integer('Facades')
garage = fields.Boolean('Garage')
garden = fields.Boolean('Garden')
garden_area = fields.Integer('Garden Area (sqm)')
garden_orientation = fields.Selection(
string='Garden Orientation',
selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')],
)
state = fields.Selection(
string='State',
selection=[('new', 'New'), ('offer_received', 'Offer Received'), ('offer_accepted', 'Offer Accepted'), ('sold', 'Sold'), ('cancelled', 'Cancelled')],
required=True,
copy=False,
default='new',
)
total_area = fields.Integer(compute='_compute_total_area', string='Total Area (sqm)')
best_price = fields.Float(compute='_compute_best_price', string='Best Price')
sales_person_id = fields.Many2one('res.users', string='Salesman', index=True, default=lambda self: self.env.user)
buyer_id = fields.Many2one('res.partner', string='Buyer', copy=False, readonly=True, compute='_compute_buyer')
tag_ids = fields.Many2many('estate.property.tag', string='Tags')
offer_ids = fields.One2many('estate.property.offer', 'property_id', string='Offers')
type_id = fields.Many2one('estate.property.type', string='Type', required=True)

## CONSTRATINS ##
_check_expected_price = models.Constraint(
'CHECK(expected_price > 0)',
'The expected price must be strictly positive.',
)

_check_selling_price = models.Constraint(
'CHECK(selling_price >= 0)',
'The selling price must be positive.',
)

@api.constrains('expected_price', 'selling_price')
def _check_expected_vs_selling_price_ratio(self):
for property in self:
if any(offer.status == 'accepted' for offer in property.offer_ids):
if float_compare(property.selling_price, property.expected_price * 0.9, precision_digits=2) < 0:
raise ValidationError(_('The selling price cannot be lower than 90 precent of the expected price: \n Selling Price: %s, Expected Price: %s') % (property.selling_price, property.expected_price))

@api.depends('offer_ids.price')
def _compute_selling_price(self):
for property in self:
property.selling_price = self.offer_ids.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.price')
def _compute_best_price(self):
for record in self:
if record.offer_ids:
record.best_price = max(record.offer_ids.mapped('price'))
else:
record.best_price = 0.0

@api.depends('offer_ids.status')
def _compute_buyer(self):
for record in self:
for offer in record.offer_ids:
if offer.status == 'accepted':
record.buyer_id = offer.partner_id
return
record.buyer_id = None

@api.onchange('garden')
def _onchange_garden(self):
if self.garden:
self.garden_area = DEFAULT_GARDEN_AREA
self.garden_orientation = 'north'
else:
self.garden_area = 0
self.garden_orientation = None

def action_cancel_property(self):
if self.state == 'sold':
raise UserError(_('Sold properties cannot be canceled.'))
self.state = 'cancelled'
for offer in self.offer_ids:
offer.status = None
return True

def action_sold_property(self):
if self.state == 'cancelled':
raise UserError(_('Cancelled properties cannot be sold.'))
self.state = 'sold'
return True

@api.ondelete(at_uninstall=False)
def _unlink_except_new_or_cancelled(self):
for record in self:
if record.state not in ('new', 'cancelled'):
raise UserError(_('You cannot delete a property unless its state is `New` or `Cancelled`.'))
72 changes: 72 additions & 0 deletions estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from datetime import date

from dateutil.relativedelta import relativedelta

from odoo import api, fields, models
from odoo.exceptions import UserError
from odoo.tools.translate import _


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

price = fields.Float('Price')
status = fields.Selection(
string='Status',
selection=[('accepted', 'Accepted'), ('refused', 'Refused')],
copy=False,
)
validity = fields.Integer('Validity (days)', default=7)
date_deadline = fields.Date(compute='_compute_deadline', inverse='_inverse_deadline', string='Deadline')
partner_id = fields.Many2one('res.partner', required=True)
property_id = fields.Many2one('estate.property', required=True)
property_type_id = fields.Many2one(related='property_id.type_id')

_check_offer_price = models.Constraint(
'CHECK(price > 0)',
'The offer price must be strictly positive.',
)

@api.depends('create_date', 'validity')
def _compute_deadline(self):
for record in self:
if record.create_date:
record.date_deadline = record.create_date + relativedelta(days=+record.validity)
else:
record.date_deadline = date.today() + relativedelta(days=+record.validity)

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

def action_accept(self):
if self.status != 'accepted':
for offer in self.property_id.offer_ids:
if offer.status == 'accepted':
raise UserError(_('Only one offer can be accepted.'))
self.status = 'accepted'
self.property_id.selling_price = self.price
self.property_id.state = 'offer_accepted'
return True

def action_refuse(self):
self.status = 'refused'
if self.property_id.state != 'offer_accepted':
self.property_id.selling_price = None
self.property_id.state = 'offer_received'
return True

@api.model
def create(self, vals_list):
for vals in vals_list:
property_id = self.env['estate.property'].browse(vals['property_id'])
max_offer = 0
if property_id.offer_ids:
max_offer = max(offer for offer in property_id.offer_ids).price
if vals['price'] > max_offer:
self.env['estate.property'].browse(vals['property_id']).state = 'offer_received'
else:
raise UserError(_("You cannot add an offer with a price lower than the maximum existing price."))
return super().create(vals_list)
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 EstateProperTag(models.Model):
_name = "estate.property.tag"
_description = "Property Tag."
_order = "name"

name = fields.Char('Property Tag', required=True)
color = fields.Integer('Color')

_check_name = models.Constraint(
'UNIQUE(name)',
'The tag 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 = "Property Type."
_order = "sequence, name"

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

_check_name = models.Constraint(
'UNIQUE(name)',
'The type must be unique.',
)

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


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

property_ids = fields.One2many('estate.property', 'sales_person_id')
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_property_model","estate_property_model","model_estate_property","base.group_user",1,1,1,1
"estate_property_type_model","estate_property_type_model","model_estate_property_type","base.group_user",1,1,1,1
"estate_property_tag_model","estate_property_tag_model","model_estate_property_tag","base.group_user",1,1,1,1
"estate_property_offer_model","estate_property_offer_model","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"?>

<odoo>

<menuitem id="estate_menu_root" name="Real Estate">
<menuitem id="estate_properties" name="Advertisements">
<menuitem id="estate_property_model_menu_action" action="estate_property_action"/>
</menuitem>
<menuitem id="settings" name="Settings">
<menuitem id="estate_property_type_model_menu_action" action="estate_property_type_action"/>
<menuitem id="estate_property_tags_model_menu_action" action="estate_property_tag_action"/>
</menuitem>
</menuitem>

</odoo>
27 changes: 27 additions & 0 deletions estate/views/estate_property_offer_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?xml version="1.0"?>
<odoo>

<record id="estate_proprty_offer_action" model="ir.actions.act_window">
<field name="name">estate.property.offer.action</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>

<record id="estate_property_offer_view_list" model="ir.ui.view">
<field name="name">estate.property.offer.view.list</field>
<field name="model">estate.property.offer</field>
<field name="arch" type="xml">
<list string="Channel" default_order="id desc" editable="bottom" decoration-danger="status == 'refused'" decoration-success="status == 'accepted'">
<field name="price"/>
<field name="partner_id" string='Partner'/>
<field name="validity"/>
<field name="date_deadline"/>
<field name="property_type_id" string="Property Type"/>
<button name="action_accept" title="Accept offer." type="object" icon="fa-check" invisible="status in ('accepted', 'refused')"/>
<button name="action_refuse" title="Refuse offer." type="object" icon="fa-times" invisible="status in ('accepted', 'refused')"/>
</list>
</field>
</record>

</odoo>
20 changes: 20 additions & 0 deletions estate/views/estate_property_tag_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0"?>
<odoo>

<record id="estate_property_tag_action" model="ir.actions.act_window">
<field name="name">estate.property.tag.action</field>
<field name="res_model">estate.property.tag</field>
<field name="view_mode">list,form</field>
</record>

<record id="estate_property_tag_view_list" model="ir.ui.view">
<field name="name">estate.property.tag.view.list</field>
<field name="model">estate.property.tag</field>
<field name="arch" type="xml">
<list string="Channel" default_order="name" editable="bottom">
<field name="name"/>
</list>
</field>
</record>

</odoo>
Loading