Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1d9543b
[ADD] estate: create a new module for Real Estate Advertisement
anden-odoo Jan 19, 2026
03dde4d
[IMP] estate: add property model
anden-odoo Jan 19, 2026
b6e4d06
[IMP] estate: add informations field
anden-odoo Jan 19, 2026
b2ac4fa
[CLN] estate: make code more conventional
anden-odoo Jan 20, 2026
bfd650f
[IMP] give access rights to base.group_user
anden-odoo Jan 20, 2026
3490325
[IMP] estate: add a 3 levels architecture and an action (Chapter 5)
anden-odoo Jan 20, 2026
b330f0e
[IMP] estate: change attributes of some fields
anden-odoo Jan 20, 2026
c7b8c7f
[IMP] estate: add new fields and attributes
anden-odoo Jan 20, 2026
e97d000
[CLN] estate: clean some indentations and useless lines
anden-odoo Jan 20, 2026
f7fb822
[IMP] estate: custom view for list and form (Chapter 6)
anden-odoo Jan 20, 2026
00761e0
[IMP] estate: add search options (chapter 6)
anden-odoo Jan 20, 2026
2b04b99
[IMP] estate: add new module for property type (Chapter 7)
anden-odoo Jan 20, 2026
476383d
[IMP] estate: add salesperson and buyer as field
anden-odoo Jan 21, 2026
af15929
[IMP] estate: add tags to property model (Chapter 7)
anden-odoo Jan 21, 2026
39246b2
[IMP] estate: add offer model (Chapter 7)
anden-odoo Jan 21, 2026
4d9b290
[IMP] estate: add total_area and best_offer fields (chapter 8)
anden-odoo Jan 21, 2026
7e72d0a
[IMP] estate: add fields for validity and deadline (Chapter 8)
anden-odoo Jan 21, 2026
94a7e2e
[CLN] estate: clean fields declaration
anden-odoo Jan 21, 2026
71bbdcd
[IMP] estate: add buttons to property form view (Chapter 9)
anden-odoo Jan 21, 2026
c72754d
[FIX] estate: solve the computation of the selling price (Chapter 9)
anden-odoo Jan 22, 2026
abd4ecb
[IMP] estate: add constraints in property and offer (Chapter 10)
anden-odoo Jan 22, 2026
c90db3e
[IMP] estate: add inline list view in type form (Chapter 11)
anden-odoo Jan 22, 2026
6ce2304
[CLN] estate: clean code to respect robodoo recommendations
anden-odoo Jan 22, 2026
b9fc1dc
[IMP] estate: add new views and ordering (Chapter 11)
anden-odoo Jan 22, 2026
d9e02b1
[IMP] estate: change the state view depending on the offers (chapter 11)
anden-odoo Jan 22, 2026
87989e0
[IMP] estate: add options and color (chapter 11)
anden-odoo Jan 22, 2026
6fed5bd
[IMP] estate: add invisible fields (Chapter 11)
anden-odoo Jan 22, 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
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/

# Not functionnal
estate/security/security.xml
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
17 changes: 17 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
'name': 'Real estate',
'author': 'anden',
'license': 'LGPL-3',
'depends': ['base'],
'category': 'Real Estate/Brokerage',
'application': True,
'data': [
# 'security/security.xml',
'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 property
from . import property_type
from . import property_tag
from . import property_offer
99 changes: 99 additions & 0 deletions estate/models/property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
from odoo import api, fields, models, exceptions
from odoo.tools.float_utils import float_compare, float_is_zero


class EstateProperty(models.Model):
_name = 'estate_property'
_description = '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=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='Garden Area (sqm)')
garden_orientation = fields.Selection(selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')])
active = fields.Boolean(default=True)
state = fields.Selection(
string='Status',
selection=[
('new', 'New'),
('offer received', 'Offer Received'),
('offer accepted', 'Offer Accepted'),
('sold', 'Sold'),
('cancelled', 'Cancelled')
],
required=True,
copy=False,
default='new',
)
property_type_id = fields.Many2one('estate_property_type', string='Property Type')
salesperson = fields.Many2one('res.users', string='Salesman', default=lambda self: self.env.user)
buyer = fields.Many2one('res.partner', copy=False)
property_tag_id = fields.Many2many('estate_property_tag', string='Property Tag')
property_offer_id = fields.One2many('estate_property_offer', 'property_id', string='Property Offer')
total_area = fields.Float(string='Total Area (sqm)', compute='_compute_area')
best_price = fields.Float(string='Best offer', compute='_compute_best_price')

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

@api.depends('property_offer_id.price')
def _compute_best_price(self):
for record in self:
record.best_price = max(record.property_offer_id.mapped('price'), default=False)

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

def action_property_sold(self):
if self.state == 'cancelled':
raise exceptions.UserError('Cancelled properties cannot be sold')
else:
self.state = 'sold'

def action_property_cancel(self):
if self.state == 'sold':
raise exceptions.UserError('Sold properties cannot be cancelled')
else:
self.state = 'cancelled'

_check_expected_price = models.Constraint(
'CHECK(0 < expected_price)',
'A property expected price must be strictly positive')

_check_selling_price = models.Constraint(
'CHECK(0 <= selling_price)',
'A property selling price must be positive')

@api.constrains('selling_price', 'expected_price')
def _check_selling_price(self):
for record in self:
if not float_is_zero(record.selling_price, 2):
if float_compare(record.selling_price, 0.9 * record.expected_price, 2) == -1:
raise exceptions.ValidationError("The selling price cannot be lower than 90% of the expected price.")

@api.onchange('property_offer_id')
def _onchange_property_offer_id(self):
for record in self:
if len(record.property_offer_id) > 0:
if record.state == 'new':
record.state = 'offer received'
elif record.state != 'cancelled':
record.state = 'new'
54 changes: 54 additions & 0 deletions estate/models/property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from odoo import api, fields, models, exceptions


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

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

@api.depends('create_date', 'validity')
def _compute_deadline(self):
for record in self:
if record.create_date:
record.date_deadline = fields.Date.add(record.create_date.date(), days=record.validity)
else:
record.date_deadline = fields.Date.add(fields.Date.today(), days=record.validity) # If no create_date we take the date of today

def _inverse_deadline(self):
for record in self:
if record.create_date:
record.validity = (record.date_deadline - record.create_date.date()).days
else:
record.validity = (record.date_deadline - fields.Date.today()).days # If no create_date we take the date of today

def action_status_accepted(self):

if len(self) > 1:
raise exceptions.UserError('Only one offer can be accepted')

if self.status != 'accepted' and 'accepted' in self.mapped('property_id.property_offer_id.status'):
raise exceptions.UserError('Another offer is already accepted')

self.status = 'accepted'
self.property_id.selling_price = self.price
self.property_id.buyer = self.partner_id
self.property_id.state = 'offer accepted'

def action_status_refused(self):
for record in self:
if record.status == 'accepted':
record.property_id.selling_price = False
record.property_id.buyer = False
record.property_id.state = 'offer received'
record.status = 'refused'

_check_offer_price = models.Constraint(
'CHECK(0 < price)',
'An offer price must be strictly positive')
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 EstatePropertyTag(models.Model):
_name = 'estate_property_tag'
_description = 'estate property tag'
_order = "name"

name = fields.Char(required=True)
color = fields.Integer("Color Index")

_name_uniq = models.Constraint(
'unique (name)',
"A property tag name must be unique",
)
16 changes: 16 additions & 0 deletions estate/models/property_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from odoo import fields, models


class EstatePropertyType(models.Model):
_name = 'estate_property_type'
_description = 'estate property type'
_order = "sequence, name"

name = fields.Char(string='Type', required=True)
sequence = fields.Integer('Sequence', default=1)
property_ids = fields.One2many('estate_property', 'property_type_id')

_name_uniq = models.Constraint(
'unique (name)',
"A property type 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_ir_group_user,ir_group_user,model_estate_property,base.group_user,1,1,1,1
access_ir_group_user_type,ir_group_user_type,model_estate_property_type,base.group_user,1,1,1,1
access_ir_group_user_tag,ir_group_user_tag,model_estate_property_tag,base.group_user,1,1,1,1
access_ir_group_user_offer,ir_group_user_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" encoding="utf-8"?>
<odoo>
<menuitem id="estate_menu_root" name="Real Estate">
<menuitem id="estate_menu_advertisements" name="Advertisements">
<menuitem id="estate_menu_properties" name="Properties" action="estate_property_action"/>
</menuitem>
<menuitem id="estate_menu_settings" name="Settings">
<menuitem id="estate_menu_property_types" name="Property Types" action="estate_property_type_action"/>
<menuitem id="estate_menu_property_tags" name="Property Tags" action="estate_property_tag_action"/>
</menuitem>
</menuitem>
</odoo>
38 changes: 38 additions & 0 deletions estate/views/estate_property_offer_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?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>
<field name="price"/>
<field name="partner_id"/>
<field name="validity"/>
<field name="date_deadline"/>
<button name="action_status_accepted" type="object" icon="fa-check" string="Accept" invisible="status=='refused' or status=='accepted'"/>
<button name="action_status_refused" type="object" icon="fa-times" string="Refuse" invisible="status=='refused' or status=='accepted'"/>
<field name="status"/>
</list>
</field>
</record>

<record id="estate_property_offer_view_form" model="ir.ui.view">
<field name="name">estate_property_view_form</field>
<field name="model">estate_property_offer</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<field name="price"/>
<field name="partner_id"/>
<field name="validity"/>
<field name="date_deadline"/>
<field name="status"/>
</group>
</sheet>
</form>
</field>
</record>

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

<record id="estate_property_tag_action" model="ir.actions.act_window">
<field name="name">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_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>
<group>
<field name="name" placeholder="Property Tag"/>
<field name="color" widget="color_picker"/>
</group>
</sheet>
</form>
</field>
</record>

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

<record id="estate_property_type_action" model="ir.actions.act_window">
<field name="name">Property type action</field>
<field name="res_model">estate_property_type</field>
<field name="view_mode">list,form</field>
</record>

<record id="estate_property_type_view_tree" 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"/>
</list>
</field>
</record>

<record id="estate_property_type_view_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>
<sheet>
<group>
<div class="oe_title">
<h1>
<field name="name" placeholder="Property Type"/>
</h1>
</div>
</group>
<notebook>
<page string="Description">
<group>
<field name="property_ids">
<list>
<field name="name"/>
<field name="expected_price"/>
<field name="state"/>
</list>
</field>
</group>
</page>
</notebook>
</sheet>
</form>
</field>
</record>

</odoo>
Loading