Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
c8637b4
[ADD] estate: Create a new module for real estate advertisment with …
stfra-odoo Dec 15, 2025
7dc2f6e
[FIX] estate: Fix required field needed in expected price and a typo
stfra-odoo Dec 15, 2025
7bcf43b
[ADD] estate: Add some access rights to the db
stfra-odoo Dec 15, 2025
ab1280b
[IMP] estate: enable UI interaction trough menus, display property d…
stfra-odoo Dec 16, 2025
a76b9fe
[IMP] estate: enrich the different views (list, form, and search) of…
stfra-odoo Dec 17, 2025
5b6bde8
[IMP] estate: introduce various new data in the estate property model…
stfra-odoo Dec 18, 2025
4a9c637
[IMP] estate: enrich the models with new fields (best price and total…
stfra-odoo Dec 19, 2025
5dafd8b
[LINT] estate: format file of estate module to remove linting issues.
stfra-odoo Dec 19, 2025
1d101e8
[IMP] estate: allow more direct interactions with the property and it…
stfra-odoo Dec 19, 2025
1138d95
[LINT] estate: add missing string to button with icon in offer view xml.
stfra-odoo Dec 22, 2025
2981aa1
[IMP] estate: check data consistency using SQL and python constraints…
stfra-odoo Dec 22, 2025
1523ade
[IMP] estate: imporove user interface with some business logic and bu…
stfra-odoo Dec 23, 2025
2d4151a
[IMP] estate: alter the standart delete method of properties, the sta…
stfra-odoo Dec 24, 2025
0b1c624
[FIX] estate: correct the state modification of a property.
stfra-odoo Dec 24, 2025
7ee77c3
[IMP] estate_account: create a new model Estate accounting to link in…
stfra-odoo Dec 24, 2025
c4a4cb8
[IMP] estate: add a basic kanban view to estate properties.
stfra-odoo Dec 24, 2025
6d9a3b9
[LINT] estate: add a explicit name to inherited model res.users.
stfra-odoo Dec 24, 2025
8a66f91
[LINT] estate_account: add explicit name to the inherited model estat…
stfra-odoo Dec 24, 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
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
20 changes: 20 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
'name': 'Estate',
'depends': [
'base_setup',
],
'installable': True,
'application': True,
'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',
],
'author': 'Odoo S.A.',
'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 res_users
from . import estate_property_type
from . import estate_property_tag
from . import estate_property_offer
from . import estate_property
116 changes: 116 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
from odoo import api, fields, models
from odoo.exceptions import UserError, ValidationError
from odoo.tools.translate import _
from odoo.tools.float_utils import float_compare, float_is_zero
from datetime import timedelta


def default_availability_date(recordset):
return fields.Date.context_today(recordset) + timedelta(days=90)


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

name = fields.Char('Name', required=True)
description = fields.Text('Description')
postcode = fields.Char('Postcode')
date_availability = fields.Date('Date availability', copy=False, default=default_availability_date)
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')
facades = fields.Integer('Facades')
garage = fields.Boolean('Garage')
garden = fields.Boolean('Garden')
garden_area = fields.Integer('Garden Area')
garden_orientation = fields.Selection(selection=[
('north', 'North'),
('south', 'South'),
('east', 'East'),
('west', 'West')
], string='Garden orientation')
total_area = fields.Integer('Total Area', compute='_compute_total_area')
active = fields.Boolean('Active', default=True)
state = fields.Selection(selection=[
('new', 'New'),
('offer_received', 'Offer Received'),
('offer_accepted', 'Offer Accepted'),
('sold', 'Sold'),
('cancelled', 'Cancelled')
], string="State", default='new', required=True, copy=False)
property_type_id = fields.Many2one('estate.property.type', string="Property type")
property_buyer_id = fields.Many2one('res.partner', string='Buyer', copy=False)
property_salesperson_id = fields.Many2one('res.users', string="Salesperson", default=lambda self: self.env.user)
property_tag_ids = fields.Many2many('estate.property.tag', string="Property tags")
property_offer_ids = fields.One2many('estate.property.offer', 'property_id', string='Offers')
best_price = fields.Float('Best Price', compute='_compute_best_price')

_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.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('property_offer_ids.price')
def _compute_best_price(self):
for record in self:
offer_prices = record.property_offer_ids.mapped('price')
record.best_price = max(offer_prices) if offer_prices else 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 check_create_offer(self, new_offer_price):
for record in self:
for offer in record.property_offer_ids:
if new_offer_price < offer.price:
raise ValidationError(_("Cannot create an offer with a lower amount than an existing offer."))
if record.state == 'new':
record.state = 'offer_received'
return True

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

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

@api.constrains('expected_price', 'selling_price')
def _check_selling_price(self):
for record in self:
if not float_is_zero(record.selling_price, precision_digits=2) and float_compare(record.selling_price, 0.9 * record.expected_price, precision_digits=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.'))

@api.ondelete(at_uninstall=False)
def _unlink_if_new_or_cancelled(self):
for record in self:
if not record.state in ['new', 'cancelled']:
raise UserError(_(
'You cannot delete a property that is in %s state.',
dict(self._fields['state']._description_selection(self.env)).get(record.state)
))
63 changes: 63 additions & 0 deletions estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from odoo import api, fields, models
from odoo.exceptions import UserError
from odoo.tools.translate import _
from dateutil.relativedelta import relativedelta


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

price = fields.Float('Price')
status = fields.Selection(selection=[
('accepted', 'Accepted'),
('refused', 'Refused')
], copy=False, string='Status')
property_buyer_id = fields.Many2one('res.partner', string="Buyer", required=True)
property_id = fields.Many2one('estate.property', string="Property", required=True)
property_type_id = fields.Many2one(related="property_id.property_type_id", store=True)
validity = fields.Integer('Validity (Days)', default=7)
date_deadline = fields.Date('Deadline', compute='_compute_date_deadline', inverse='_inverse_validity')

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

@api.depends('validity')
def _compute_date_deadline(self):
for record in self:
starting_date = record.create_date if record.create_date else fields.Date.context_today(self)
record.date_deadline = starting_date + relativedelta(days=record.validity)

def _inverse_validity(self):
for record in self:
starting_date = fields.Date.to_date(record.create_date) if record.create_date else fields.Date.context_today(self)
record.validity = (record.date_deadline - starting_date).days

def action_accept(self):
for record in self:
match record.property_id.state:
case 'offer_accepted':
raise UserError(_('This property already has an offer accepted'))
case 'sold':
raise UserError(_('Sold properties cannot accept offers'))
case 'cancelled':
raise UserError(_('Cancelled properties cannot accept other offers'))
record.status = 'accepted'
record.property_id.state = 'offer_accepted'
record.property_id.property_buyer_id = record.property_buyer_id
record.property_id.selling_price = record.price

def action_refuse(self):
for record in self:
record.status = 'refused'
record.property_id.property_buyer_id = None
record.property_id.selling_price = 0

@api.model
def create(self, vals_list):
for val in vals_list:
self.env['estate.property'].browse(val['property_id']).check_create_offer(val['price'])
return super().create(vals_list)
14 changes: 14 additions & 0 deletions estate/models/estate_property_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from odoo import fields, models


class EstatePropertyTag(models.Model):
_name = "estate.property.tag"
_description = "A property tag"
_order = "name asc"

name = fields.Char('Property tag', required=True)
_unique_name = models.Constraint(
'unique (name)',
'A property tag name must be unique.'
)
color = fields.Integer('Color')
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 = "A property type is, for example, a house or an apartment. It is a standard business need to categorize properties according to their type, especially to refine filtering."
_order = "name asc"

name = fields.Char('Property type', required=True)
_unique_name = models.Constraint(
'unique (name)',
'A property type name must be unique.'
)

property_ids = fields.One2many('estate.property', 'property_type_id', string='Properties')
sequence = fields.Integer('Sequence', default=1, help="Used to order types. Lower is better.")
offer_ids = fields.One2many('estate.property.offer', 'property_type_id')
offer_count = fields.Integer('Offer count', compute='_compute_offer_count')

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


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

property_ids = fields.One2many('estate.property', 'property_salesperson_id', domain="['|', ('state', '=', 'new'), ('state', '=', 'offer_received')]")
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_real_estate_platform,estate_property,model_estate_property,base.group_user,1,1,1,1
access_real_estate_type,estate_property_type,model_estate_property_type,base.group_user,1,1,1,1
access_real_estate_tag,estate_property_tag,model_estate_property_tag,base.group_user,1,1,1,1
access_real_estate_offer,estate_property_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"?>
<odoo>
<menuitem id="estate_menu_root" name="Real Estate">
<menuitem id="estate_advertisements" name="Advertisements">
<menuitem id="estate_property_menu_action" action="estate_property_action"/>
</menuitem>
<menuitem id="estate_settings" 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>
42 changes: 42 additions & 0 deletions estate/views/estate_property_offer_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?xml version="1.0"?>
<odoo>
<record id="estate_property_offer_view_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="Estate Property Offer">
<sheet>
<group>
<field name="price"/>
<field name="property_buyer_id" string="Partner"/>
<field name="status"/>
<field name="validity"/>
<field name="date_deadline"/>
</group>
</sheet>
</form>
</field>
</record>

<record id="estate_property_offer_view_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="Estate property offer" editable="bottom" decoration-danger="status == 'refused'" decoration-success="status == 'accepted'">
<field name="price"/>
<field name="property_buyer_id" string="Partner"/>
<field name="validity"/>
<field name="date_deadline"/>
<button name="action_accept" type="object" icon="fa-check" string=" " invisible="status"/>
<button name="action_refuse" type="object" icon="fa-times" string=" " invisible="status"/>
</list>
</field>
</record>

<record id="estate_property_offer_action" model="ir.actions.act_window">
<field name="name">Property Offers</field>
<field name="res_model">estate.property.offer</field>
<field name="view_mode">list,form</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"?>
<odoo>
<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 string="Estate Property Type">
<sheet>
<h1><group>
<field name="name" string="Tag"/>
</group></h1>
</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,form</field>
</record>
</odoo>
Loading