Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
eff5466
[ADD] estate: Added the new real estate module
ripil-odoo Apr 2, 2026
d51a356
[CLN] estate: Code cleanup in the new real estate module
ripil-odoo Apr 3, 2026
365414a
[ADD] estate: Defined the estate properties model with basic fields
ripil-odoo Apr 3, 2026
cbdd03e
[ADD] estate: Setting Access Rights for the estate module
ripil-odoo Apr 3, 2026
29b692e
[CLN] estate: Code cleanup in real estate module
ripil-odoo Apr 3, 2026
390038b
[ADD] estate: Added menu and an action in estate module
ripil-odoo Apr 6, 2026
1613039
[ADD] estate: Availability Date Calculation in estate module
ripil-odoo Apr 6, 2026
13bb5d4
[ADD] estate: Custom list view in estate module (partial)
ripil-odoo Apr 7, 2026
9b9b1a4
[ADD] estate: Custom list view in estate module
ripil-odoo Apr 8, 2026
105b6e6
[ADD] estate: Custom field for form view in estate module (partial)
ripil-odoo Apr 8, 2026
be1dc76
[IMP] estate: Custom form view and search view for estate module
ripil-odoo Apr 9, 2026
6c70217
[CLN] estate: Code format cleanup in estate module
ripil-odoo Apr 9, 2026
0fb818e
[IMP] estate: Custom search view with filter and group by in estate m…
ripil-odoo Apr 10, 2026
c849d1c
[ADD] estate: Created new property type model in estate module
ripil-odoo Apr 13, 2026
af9d554
[FIX] estate: Missing view in manifest in estate module
ripil-odoo Apr 13, 2026
82c6469
[ADD] estate: New Property Type and Property Tag models in real estat…
ripil-odoo Apr 14, 2026
8d18d21
[CLN] estate: Code Cleanup in estate_properties.py
ripil-odoo Apr 14, 2026
e7ff64c
[ADD] estate: New Property Offer model in estate module
ripil-odoo Apr 15, 2026
69fac9f
[IMP] estate: Computed fields in estate module
ripil-odoo Apr 15, 2026
9172235
[IMP] estate: Method decorators in estate module
ripil-odoo Apr 16, 2026
076b2df
[IMP] estate: Button actions in estate module
ripil-odoo Apr 17, 2026
ee96fad
[IMP] estate: UI decorations in estate module
ripil-odoo Apr 17, 2026
62bb00b
[IMP] estate: Constraints in estate module and code review #1
ripil-odoo Apr 27, 2026
6fcabca
[IMP] estate: SQL and python constraints in estate module
ripil-odoo Apr 28, 2026
88248e5
[IMP] estate: widget, _order, and invisible in estate module
ripil-odoo Apr 28, 2026
5c0578a
[IMP] estate: UI mods in estate module
ripil-odoo Apr 29, 2026
a757667
[IMP] estate: Stat button in estate module
ripil-odoo Apr 29, 2026
48c7de9
[IMP] estate: Stat Button in estate module finished
ripil-odoo Apr 30, 2026
ade3985
[IMP] estate: Experimenting with stat buttons in estate module
ripil-odoo Apr 30, 2026
8068552
[FIX] estate: Action not found error in estate module
ripil-odoo May 1, 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
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': 'Estate',
'category': 'Real Estate',
'sequence': 1,
'summary': 'Making the real estate app from tutorials',
'website': 'www.odoo.com',
'depends': [
'base',
],
'data': [
'security/ir.model.access.csv',
'views/estate_properties_view.xml',
'views/estate_property_offer_view.xml',
'views/estate_property_type_view.xml',
'views/estate_property_tag_view.xml',
'views/estate_menus.xml',
],
'installable': True,
'application': True,
Comment on lines +18 to +19
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

What's the purpose of using 'application': True and 'installable': True?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

  1. 'application': True makes the estate module list as an app and visible with the Apps filter, otherwise it is not listed as an app just a module
  2. 'installable': True allows the module to be installed ('Activate' button)

'author': 'Rini Pillai',
'license': 'LGPL-3',
}
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_properties
from . import estate_property_type
from . import estate_property_tag
from . import estate_property_offer
211 changes: 211 additions & 0 deletions estate/models/estate_properties.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import logging
from dateutil import relativedelta as rd

from odoo import fields, models, api
from odoo.exceptions import UserError, ValidationError


_logger = logging.getLogger(__name__)


def _get_availability_date(self):
# _logger.info("!!! %s", self)
return fields.Date.today() + rd.relativedelta(months=3)


def _get_salesperson(self):
# _logger.info(self.env.user.name)
# _logger.info(self.env.user.id)
# _logger.info(self.env.user)
return self.env.user


class EstateProperties(models.Model):
_name = 'estate.properties'
_description = 'Real Estate Properties'
_order = 'sequence'

active = fields.Boolean(help="Should the property be listed?", default=True)
bedrooms = fields.Integer(default=2)
best_price = fields.Float(compute='_compute_best_price')
buyer_id = fields.Many2one(comodel_name='res.partner', copy=False)
commission = fields.Integer(compute='_compute_commission', store=True)
date_availability = fields.Date(string="Availability Date", copy=False, default=lambda self: fields.Date.add(fields.Date.context_today(self), months=3))
description = fields.Text()
expected_price = fields.Float(string="Expected Price", required=True)
facades = fields.Integer()
garage = fields.Boolean(string="Has Garage?", help="Does the proeprty have a garage?")
garden = fields.Boolean(string="Has Garden?", help="Does the property have a garden?")
garden_area = fields.Integer()
garden_orientation = fields.Selection(
[
('north', "North"),
('south', "South"),
('east', "East"),
('west', "West")
],
help="Directional orientation of the garden of the property shown"
)
living_area = fields.Integer()
name = fields.Char(string="Property Name", required=True)
offer_ids = fields.One2many(comodel_name='estate.property.offer', inverse_name='property_id')
postcode = fields.Char()
price_gap = fields.Float(compute='_compute_price_gap')
property_type_colour = fields.Selection(related='property_type_id.colour', readonly=False)
property_type_id = fields.Many2one(comodel_name='estate.property.type')
salesperson_id = fields.Many2one(comodel_name='res.partner', default=lambda self: self.env.user.partner_id)
# salesperson = fields.Char(default=_get_salesperson)
selling_price = fields.Float(readonly=True, copy=False)
sequence = fields.Integer()
state = fields.Selection(
[
('new', "New"),
('offer_received', "Offer Received"),
('offer_accepted', "Offer Accepted"),
('sold', "Sold"),
('cancelled', "Cancelled")
],
required=True, default='new', copy=False, string="Status"
)
tag_ids = fields.Many2many(comodel_name='estate.property.tag')
total_area = fields.Integer(compute="_compute_total_area")

_check_expected_price = models.Constraint(
'CHECK (expected_price > 0 AND selling_price >= 0)',
"Price should strictly be positive",
)

@api.constrains('selling_price')
def _check_selling_price(self):
# breakpoint()
# _logger.error(self.id)
for property in self:
sp_threshold = 0.9 * property.expected_price
# _logger.error(sp_threshold)
# _logger.error(property.selling_price)
# _logger.error(property.expected_price)
# _logger.error(property.id)
if property.selling_price < sp_threshold:
raise ValidationError("Selling price cannot be less than 90% of the expected price")

@api.depends('living_area', 'garden_area')
def _compute_total_area(self):
# _logger.error(self)
for property in self:
# _logger.error(property._fields)
# _logger.error(property)
property.total_area = property.living_area + property.garden_area

@api.depends('offer_ids.price')
def _compute_best_price(self):
for property in self:
# _logger.error(self.mapped('offer_ids.price'))
offer_prices = property.mapped('offer_ids.price')
# _logger.error(offer_prices)
# _logger.error(max(offer_prices))
property.best_price = max(offer_prices) if offer_prices else 0

# @api.constrains('expected_price')
# def _check_expected_price(self):
# for property in self:
# if property.expected_price < 0:
# raise UserError("Value cannot be less than zero.")

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

# @api.constrains('date_availability')
# def _check_date_availability(self):
# for property in self:
# if property.date_availability < fields.Date.context_today(property):
# raise ValidationError("Past dates not allowed")

@api.depends('best_price', 'expected_price')
def _compute_price_gap(self):
for property in self:
if property.best_price and property.expected_price:
property.price_gap = property.best_price - property.expected_price
else:
property.price_gap = 0

@api.onchange('state')
def _onchnage_state(self):
if self.state == 'cancelled':
self.active = False
else:
self.active = True

def property_cancelled(self):
for property in self:
if property.state == 'cancelled':
raise UserError("Property already cancelled")
elif property.state != 'sold':
property.state = 'cancelled'
property.active = False
else:
raise UserError("A sold property cannot be cancelled")
return True

def property_sold(self):
for property in self:
if property.state == 'sold':
raise UserError("Property already sold")
elif property.state != 'cancelled':
if property.buyer_id:
property.state = 'sold'
else:
raise UserError("No buyer for this property yet")
else:
raise UserError("A cancelled property cannot be sold")
return True

@api.depends('selling_price')
def _compute_commission(self):
for property in self:
if property.selling_price:
property.commission = property.selling_price * 0.06

def property_accept(self):
# breakpoint()
# best_offers = []
# for property in self.offer_ids:
# best_offers.append(property.price)
# _logger.error(best_offers)
# property_to_accept = max(best_offers)
# _logger.error(property_to_accept)
# if self.offer_ids:
# best = self.offer_ids.search([('price', '=', self.best_price), ('status', 'not in', ['refused'])])
# if best:
# best.offer_accepted()
# return
# best = self.offer_ids.search([('price', '<', self.best_price), ('status', 'not in', ['refused'])], 0, 1, order='price DESC')
# best.ensure_one()
# best.offer_accepted()
# # _logger.error(best)
# else:
# raise ValidationError("No offers listed for this property")
# for property in self.offer_ids:
# if property.status == 'accepted':
# pass
# else:
# property.status = 'refused'
self.ensure_one()
best_offer = self.offer_ids.filtered(lambda offer: offer.status not in ['accepted', 'refused'])

if not best_offer:
raise ValidationError("No offers listed for this property")

best_offer = best_offer.sorted(lambda offer: offer.price, reverse=True)[0]
return best_offer.offer_accepted()

@api.onchange('offer_ids')
def offer_received_state(self):
if self.offer_ids and self.state == 'new':
self.state = 'offer_received'
104 changes: 104 additions & 0 deletions estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import datetime
import logging

from odoo import api, fields, models
from odoo.exceptions import UserError
from odoo.orm.utils import ValidationError


_logger = logging.getLogger(__name__)


class EstatePropertyOffer(models.Model):
_name = 'estate.property.offer'
_description = 'Real Estate Property Offers'
_order = 'price desc'

deadline = fields.Date()
partner_id = fields.Many2one(comodel_name='res.partner', required=True)
price = fields.Float()
property_id = fields.Many2one(comodel_name='estate.properties', readonly=True)
property_type = fields.Char(related='property_id.property_type_id.type')
property_type_id = fields.Many2one(related='property_id.property_type_id')
status = fields.Selection(
[
('refused', "Refused"),
('accepted', "Accepted")
],
copy=False
)
validity = fields.Integer(compute="_compute_validity", inverse="_inverse_deadline", default=7)

_check_offer_price = models.Constraint(
'CHECK (price > 0)',
"Offer price should be positive"
)

@api.depends('deadline')
def _compute_validity(self):
for offer in self:
offer.validity = (offer.deadline - offer.create_date.date()).days if offer.deadline and offer.create_date else 7

@api.depends('validity')
def _inverse_deadline(self):
for offer in self:
# _logger.error(fields.Date.context_today(offer) + datetime.timedelta(days=offer.validity))
# _logger.error(offer._fields)
start_date = offer.create_date.date() if offer.create_date else fields.Date.context_today(offer)
offer.deadline = start_date + datetime.timedelta(days=offer.validity)

# @api.constrains('price')
# def _check_price(self):
# for property in self:
# if property.price < 0:
# raise ValidationError("Value cannot be less than zero.")

# @api.constrains('deadline')
# def _check_deadline(self):
# for property in self:
# if property.deadline < fields.Date.context_today(property):
# raise ValidationError("Past dates not allowed")

def _refuse_remaining_offers(self, offer_id, all_offers):
# _logger.error(all_offers)
for offer in all_offers:
if offer.id != offer_id:
offer.status = 'refused'

def offer_accepted(self):
# offer_id = 0
# breakpoint()
# for offer in self:
# if offer.status == 'accepted' and offer.property_id.state == 'offer_accepted':
# raise UserError("Property already accepted!")
# offer.status = 'accepted'
# all_offers = offer.property_id.offer_ids
# # _logger.error(all_offers)
# offer_id = offer.id
# offer._refuse_remaining_offers(offer_id, all_offers)
# offer.property_id.buyer_id = offer.partner_id
# offer.property_id.selling_price = offer.price
# offer.property_id.state = 'offer_accepted'
# return True
self.ensure_one()
property = self.property_id
if property.state == 'offer_accepted' and self.status == 'accepted':
raise ValidationError("Property already accepted!")
remaining_offers = property.offer_ids - self
remaining_offers.write({
'status': 'refused'
})
self.status = 'accepted'
property.write({
'buyer_id': self.partner_id,
'selling_price': self.price,
'state': 'offer_accepted',
})
return True

def offer_refused(self):
for offer in self:
if offer.status == 'refused':
raise UserError("Property already refused!")
offer.status = 'refused'
return True
20 changes: 20 additions & 0 deletions estate/models/estate_property_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import random

from odoo import fields, models


class EstatePropertyTag(models.Model):
_name = 'estate.property.tag'
_description = 'Estate property tags'
_order = 'name'

def _get_default_color(self):
return random.randint(1, 11)

name = fields.Char(required=True)
color = fields.Integer(default=_get_default_color)

_check_name = models.Constraint(
'UNIQUE (name)',
"Property Tag should be unique",
)
Loading