Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
a80ef97
[ADD] estate: module skeleton
venpr-odoo Feb 5, 2026
db2e80c
[ADD] estate: basic property model
venpr-odoo Feb 5, 2026
8e03425
[ADD] estate: basic property views
venpr-odoo Feb 5, 2026
472a467
[ADD] estate: basic security rules
venpr-odoo Feb 5, 2026
4a2cb03
[IMP] estate: add menus, actions, and custom list/form/search views
venpr-odoo Feb 6, 2026
2108661
[FIX] estate: clean up access rules and add property views
venpr-odoo Feb 9, 2026
1d20518
[ADD] estate: property types, tags and offers
venpr-odoo Feb 10, 2026
42c643c
[ADD] estate: property types, tags and offers
venpr-odoo Feb 10, 2026
9136ec4
[IMP] estate: add property types, tags and offers
venpr-odoo Feb 10, 2026
5ee2f9c
[FIX] estate: flake8 warning (W391)
venpr-odoo Feb 11, 2026
efcf8bc
[REM] estate: remove custom security groups and cleanup
venpr-odoo Feb 11, 2026
6552201
[IMP] estate: implemented computed fields, inverse logic for offer de…
venpr-odoo Feb 12, 2026
5a7db48
[IMP] estate: 9th chacpter add property offers and implement accept/r…
venpr-odoo Feb 13, 2026
76e61c7
[IMP] estate: 9th chacpter add property offers and implement accept/r…
venpr-odoo Feb 13, 2026
3f647db
[IMP] estate: 9th chacpter add property offers and implement accept/r…
venpr-odoo Feb 13, 2026
451a4d7
[IMP] estate: 9th chacpter add property offers and implement accept/r…
venpr-odoo Feb 13, 2026
484a219
[IMP] estate: 9th chacpter add property offers and implement accept/r…
venpr-odoo Feb 13, 2026
14460e8
[IMP] estate: 9th chacpter add property offers and implement accept/r…
venpr-odoo Feb 13, 2026
71d034a
[FIX] estate: fixixng linter issues and unlink override method
venpr-odoo Feb 16, 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
18 changes: 18 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "Real Estate",
"version": "1.0",
"category": "Tutorials",
"summary": "Real Estate Advertisement Module",
"author": "Prasad Venkat",
"license": "LGPL-3",
"depends": ["base"],
"data": [
"security/ir.model.access.csv",
"views/estate_property_type_views.xml",
"views/estate_property_views.xml",
"views/estate_property_tag_views.xml",
"views/estate_menus.xml",
],
"application": True,
"installable": True,
}
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
114 changes: 114 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
from datetime import timedelta

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


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

Choose a reason for hiding this comment

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

Suggested change
_description = "Real Estate Property"
_description = "Real Estate Property"


name = fields.Char(required=True)
description = fields.Text()
postcode = fields.Char()
date_availability = fields.Date(
default=lambda self: fields.Date.today() + timedelta(days=90),
copy=False,
)
expected_price = fields.Float(required=True, default=0.0)
selling_price = fields.Float(readonly=True, copy=False)
best_price = fields.Float(compute="_compute_best_price")
bedrooms = fields.Integer(default=2, copy=False)
living_area = fields.Integer()
facades = fields.Integer()
garage = fields.Boolean()
garden = fields.Boolean()
garden_area = fields.Integer()
total_area = fields.Float(compute="_compute_total_area")
garden_orientation = fields.Selection(
[
("north", "North"),
("south", "South"),
("east", "East"),
("west", "West"),
Comment on lines +30 to +33

Choose a reason for hiding this comment

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

Suggested change
("north", "North"),
("south", "South"),
("east", "East"),
("west", "West"),
('north', "North"),
('south', "South"),
('east', "East"),
('west', "West"),

Try keeping key in lower case

]
)
active = fields.Boolean(default=True)
state = fields.Selection([
('new', 'New'),
('offer_received', 'Offer Received'),
('offer_accepted', 'Offer Accepted'),
('sold', 'Sold'),
('cancelled', 'Cancelled'),
], compute="_compute_state", store=True)
property_type_id = fields.Many2one(
"estate.property.type",
string="Property Type",
)
buyer_id = fields.Many2one(
"res.partner",
string="Buyer",
copy=False,
readonly=True,
)
user_id = fields.Many2one(
"res.users",
string="Salesperson",
default=lambda self: self.env.user,
)
tag_ids = fields.Many2many("estate.property.tag", string="Tags")
offer_ids = fields.One2many(
"estate.property.offer",
"property_id",
string="Offers",
)

@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:
prices = record.offer_ids.mapped("price")
record.best_price = max(prices) if prices else 0.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 = False

@api.depends('offer_ids.status')
def _compute_state(self):
for property_rec in self:
offers = property_rec.offer_ids
if property_rec.state in ['sold', 'cancelled']:
continue
if not offers:
property_rec.state = 'new'
elif any(o.status == 'accepted' for o in offers):
property_rec.state = 'offer_accepted'
else:
property_rec.state = 'offer_received'

def action_cancel(self):
for record in self:
if record.state == "sold":
raise UserError("Sold property cannot be cancelled.")
record.state = "cancelled"

def action_sold(self):
for record in self:
if record.state == 'cancelled':
raise UserError("Cancelled property cannot be sold.")
accepted_offer = record.offer_ids.filtered(
lambda o: o.status == 'accepted'
)
if not accepted_offer:
raise UserError("You must accept an offer before selling the property.")
record.state = 'sold'
92 changes: 92 additions & 0 deletions estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
from datetime import timedelta

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


class EstatePropertyOffer(models.Model):
_name = "estate.property.offer"
_description = "Estate Property Offer"

price = fields.Float(string="Offer Price")
status = fields.Selection(
[
("pending", "Pending"),
("accepted", "Accepted"),
("refused", "Refused"),
],
copy=False,
default="pending",
)
partner_id = fields.Many2one(
"res.partner",
string="Buyer",
required=True,
)
property_id = fields.Many2one(
"estate.property",
string="Property",
required=True,
)
validity = fields.Integer(default=7)
date_deadline = fields.Date(
compute="_compute_date_deadline",
inverse="_inverse_date_deadline",
)

@api.depends("create_date", "validity")
def _compute_date_deadline(self):
for record in self:
create_date = record.create_date.date() if record.create_date else fields.Date.today()
record.date_deadline = create_date + timedelta(days=record.validity)

def _inverse_date_deadline(self):
for record in self:
create_date = record.create_date.date() if record.create_date else fields.Date.today()
record.validity = (record.date_deadline - create_date).days

@api.model_create_multi
def create(self, vals_list):
offers = super().create(vals_list)
for offer in offers:
property_rec = offer.property_id
if property_rec.state in ['offer_accepted', 'sold', 'cancelled']:
raise UserError("Cannot create offer for this property.")
return offers

def action_accept(self):
for offer in self:
if offer.property_id.state in ['sold', 'cancelled']:
raise UserError("You cannot accept an offer on a sold or cancelled property.")
accepted_offer = offer.property_id.offer_ids.filtered(
lambda o: o.status == "accepted" and o != offer
)
if accepted_offer:
raise UserError("Only one offer can be accepted for a property.")
offer.status = "accepted"
offer.property_id.write({
'selling_price': offer.price,
'buyer_id': offer.partner_id.id,
'state': 'offer_accepted'
})

def action_refuse(self):
for offer in self:
property_rec = offer.property_id
if property_rec.state in ['sold', 'cancelled']:
raise UserError("You cannot refuse an offer on a sold or cancelled property.")
if offer.status == "accepted":
offer.status = "refused"
property_rec.write({
'selling_price': 0.0,
'buyer_id': False,
})
other_pending = property_rec.offer_ids.filtered(
lambda o: o.status == 'pending'
)
if other_pending:
property_rec.state = 'offer_received'
else:
property_rec.state = 'new'
else:
offer.status = "refused"
8 changes: 8 additions & 0 deletions estate/models/estate_property_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from odoo import models, fields


class EstatePropertyTag(models.Model):
_name = "estate.property.tag"
_description = "Estate Property Tag"

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


class EstatePropertyType(models.Model):
_name = "estate.property.type"
_description = "Estate Property Type"

name = fields.Char(required=True)
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_estate_property_user,estate.property user,model_estate_property,base.group_user,1,1,1,1
access_estate_property_type_user,estate.property.type user,model_estate_property_type,base.group_user,1,1,1,1
access_estate_property_tag_user,estate.property.tag user,model_estate_property_tag,base.group_user,1,1,1,1
access_estate_property_offer_user,estate.property.offer user,model_estate_property_offer,base.group_user,1,1,1,1
8 changes: 8 additions & 0 deletions estate/views/estate_menus.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<menuitem id="estate_menu_root" name="Real Estate" sequence="10" />
<menuitem id="estate_menu_properties" name="Properties" parent="estate_menu_root" action="estate_property_action" sequence="10" />
<menuitem id="estate_menu_settings" name="Settings" parent="estate_menu_root" sequence="20" />
<menuitem id="estate_property_type_menu" name="Property Types" parent="estate_menu_settings" action="estate_property_type_action" sequence="10" />
<menuitem id="estate_property_tag_menu" name="Property Tags" parent="estate_menu_settings" action="estate_property_tag_action" sequence="20" />
</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 @@
<odoo>
<record id="estate_property_offer_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>
<field name="price"/>
<field name="partner_id"/>
<field name="status"/>
<field name="validity"/>
<field name="date_deadline"/>
<button name="action_accept"
type="object"
icon="fa-check"
class="btn-success"
attrs="{'invisible': [('status', '!=', 'pending')]}"/>
<button name="action_refuse"
type="object"
icon="fa-times"
class="btn-danger"
attrs="{'invisible': [('status', '!=', 'pending')]}"/>
</list>
</field>
</record>
<record id="estate_property_offer_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>
<sheet>
<group>
<field name="price"/>
<field name="partner_id"/>
<field name="status"/>
<field name="validity"/>
<field name="date_deadline"/>
</group>
</sheet>
</form>
</field>
</record>
</odoo>
30 changes: 30 additions & 0 deletions estate/views/estate_property_tag_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<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>
<record id="estate_property_tag_list" model="ir.ui.view">
<field name="name">estate.property.tag.list</field>
<field name="model">estate.property.tag</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
</list>
</field>
</record>
<record id="estate_property_tag_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"/>
</group>
</sheet>
</form>
</field>
</record>
</odoo>
31 changes: 31 additions & 0 deletions estate/views/estate_property_type_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<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>
<record id="estate_property_type_list" 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="name"/>
</list>
</field>
</record>
<record id="estate_property_type_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>
<field name="name"/>
</group>
</sheet>
</form>
</field>
</record>
</odoo>

Loading