Skip to content

[ADD] real_estate: add complete real estate management module #839

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 6 commits into
base: 18.0
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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 estate_account/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from . import models

Choose a reason for hiding this comment

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

Missing licensing info

Applies throughout the diff

Copy link
Author

@metu-odoo metu-odoo Jul 10, 2025

Choose a reason for hiding this comment

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

-- coding: utf-8 --
Part of Odoo. See LICENSE file for full copyright and licensing details.
sir do you mean this ?

Copy link
Author

Choose a reason for hiding this comment

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

runbot is failing because of these lines

Choose a reason for hiding this comment

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

it will be failing because of encode declaration and not for the licensing part.
you can remove the encoding part and keep the licensing part

11 changes: 11 additions & 0 deletions estate_account/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

{
"name": "Estate Account",
"version": "1.0",
"depends": ["real_estate", "account"],
"category": "Accounts for Estate",
"license": "LGPL-3",
"installable": True,
"application": True,
}
3 changes: 3 additions & 0 deletions estate_account/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from . import estate_property
50 changes: 50 additions & 0 deletions estate_account/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from odoo import Command, models


class Property(models.Model):
_inherit = "estate.property"

def action_sold(self):
for property in self:
journal = self.env["account.journal"].search(
[("type", "=", "sale")], limit=1
)

commission = property.selling_price * 0.06
discount = property.selling_price * 0.05
invoice_lines = [
Command.create(
{
"name": "6% Commission",
"quantity": 1,
"price_unit": commission,
}
),
Command.create(
{
"name": "Administrative Fees",
"quantity": 1,
"price_unit": 100.00,
}
),
Command.create(
{
"name": "Discount",
"quantity": 1,
"price_unit": -discount,
}
),
]

invoice_vals = {
"partner_id": property.buyer_id.id,
"move_type": "out_invoice",
"journal_id": journal.id,
"invoice_line_ids": invoice_lines,
}

self.env["account.move"].create(invoice_vals)

return super().action_sold()
3 changes: 3 additions & 0 deletions real_estate/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from . import models
20 changes: 20 additions & 0 deletions real_estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

{
"name": "Dream Homes",
"version": "1.0",
"depends": ["base"],
"category": "Real Estate/Brokerage",
"data": [
"security/ir.model.access.csv",
"views/estate_property_views.xml",
"views/estate_property_offer_views.xml",
"views/estate_property_type_views.xml",
"views/estate_property_tag_views.xml",
"views/estate_property_menus.xml",
"views/res_users_views.xml",
],
"license": "LGPL-3",
"installable": True,
"application": True,
}
7 changes: 7 additions & 0 deletions real_estate/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from . import estate_property
from . import estate_property_offers
from . import estate_property_tags
from . import estate_property_types
from . import res_users
131 changes: 131 additions & 0 deletions real_estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from odoo import api, fields, models
from dateutil.relativedelta import relativedelta
from odoo.exceptions import UserError, ValidationError
from odoo.tools.float_utils import float_compare, float_is_zero


class EstateProperty(models.Model):
_name = "estate.property"
_description = "This is a real estate module"
_order = "id desc"

name = fields.Char(string="Property Name", required=True)
description = fields.Text(string="Description")
postcode = fields.Char(string="Postcode")
expected_price = fields.Float(string="Expected Price", required=True)
selling_price = fields.Float(string="Selling Price", readonly=True, copy=False)
bedrooms = fields.Integer(string="Total Bedrooms", default=2)
living_area = fields.Integer(string="Living Area (sqm)")
facades = fields.Integer(string="Facades")
garage = fields.Boolean(string="Garage")
garden = fields.Boolean(string="Garden")
garden_area = fields.Integer(string="Garden Area (sqm)")
active = fields.Boolean(string="Is Active", default=True)
buyer_id = fields.Many2one(comodel_name="res.partner", string="Buyer", copy=False)
total_area = fields.Float(compute="_compute_total_area", string="Total Area")
best_offer = fields.Float(compute="_compute_best_price", string="Best Offer")
date_availability = fields.Date(
string="Availability From",
default=lambda self: fields.Date.today() + relativedelta(months=3),
copy=False,
)
estate_property_offer_ids = fields.One2many(
comodel_name="estate.property.offers",
inverse_name="property_id",
string="Offers",
)
seller_id = fields.Many2one(
comodel_name="res.users",
string="SalesPerson",
default=lambda self: self.env.user,
)
estate_property_type_id = fields.Many2one(
comodel_name="estate.property.types", string="Property Type"
)
estate_property_tag_ids = fields.Many2many(
comodel_name="estate.property.tags", string="Tags"
)
garden_orientation = fields.Selection(
selection=[
("north", "North"),
("south", "South"),
("east", "East"),
("west", "West"),
],
string="Garden Orientation",
)
state = fields.Selection(
selection=[
("new", "New"),
("offer_received", "Offer Received"),
("offer_accepted", "Offer Accepted"),
("sold", "Sold"),
("cancelled", "Cancelled"),
],
copy=False,
required=True,
default="new",
string="State",
)
_sql_constraints = [
(
"check_expected_price_positive",
"CHECK(expected_price > 0)",
"Expected price must be strictly 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("estate_property_offer_ids")
def _compute_best_price(self):
for record in self:
prices = record.estate_property_offer_ids.mapped("price")
record.best_offer = 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

def action_sold(self):
for record in self:
if not record.state == "offer_accepted":
raise UserError("Accept an offer first")
if record.state == "cancelled":
raise UserError("You cannot mark a cancelled property as sold.")
record.state = "sold"

def action_cancel(self):
for record in self:
if record.state == "sold":
raise UserError("You cannot mark a sold property as cancelled.")
record.state = "cancelled"

@api.constrains("expected_price", "selling_price")
def _check_selling_price(self):
for record in self:
if float_is_zero(record.selling_price, precision_digits=2):
continue
min_price = record.expected_price * 0.9
if float_compare(record.selling_price, min_price, precision_digits=2) < 0:
raise ValidationError(
"Selling price must be at least 90% of the expected price."
)

@api.ondelete(at_uninstall=False)
def _check_deletion_state(self):
for rec in self:
if rec.state not in ["new", "cancelled"]:
raise UserError(
"Only properties in 'New' or 'Cancelled' state can be deleted."
)
86 changes: 86 additions & 0 deletions real_estate/models/estate_property_offers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

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


class EstatePropertyOffers(models.Model):
_name = "estate.property.offers"
_description = "Estate Property Offers"
_order = "price desc"

price = fields.Float()
partner_id = fields.Many2one(comodel_name="res.partner", required=True)
property_id = fields.Many2one(comodel_name="estate.property", required=True)
validity = fields.Integer(default="7")
property_type_id = fields.Many2one(
related="property_id.estate_property_type_id", store=True
)

offer_deadline = fields.Date(
compute="_compute_date_deadline", inverse="_inverse_date_deadline"
)
status = fields.Selection(
selection=[("accepted", "Accepted"), ("refused", "Refused")]
)

@api.depends("create_date", "validity")
def _compute_date_deadline(self):
for record in self:
if record.create_date and record.validity:
record.offer_deadline = fields.Date.add(
record.create_date, days=record.validity
)
else:
record.offer_deadline = fields.Date.add(
fields.Date.today(), days=record.validity
)

def _inverse_date_deadline(self):
for record in self:
if record.create_date and record.offer_deadline:
record.validity = (
record.offer_deadline - fields.Date.to_date(record.create_date)
).days

def action_accept_offer(self):
for offer in self:
accepted_offer = self.env["estate.property.offers"].search(
[
("property_id", "=", offer.property_id.id),
("status", "=", "accepted"),
]
)
if accepted_offer:
raise UserError("Only one offer can be accepted per property.")
offer.status = "accepted"
offer.property_id.selling_price = offer.price
offer.property_id.buyer_id = offer.partner_id
offer.property_id.state = "offer_accepted"

def action_refuse_offer(self):
for offer in self:
offer.status = "refused"

_sql_constraints = [
(
"check_offer_price_positive",
"CHECK(price > 0)",
"Offer price must be strictly positive.",
)
]

@api.model_create_multi
def create(self, vals):
for val in vals:
property_id = self.env["estate.property"].browse(val["property_id"])

if property_id.state == "new":
property_id.state = "offer_received"

if val["price"] <= property_id.best_offer:
raise UserError(
f"The offer must be higher than {property_id.best_offer}"
)

return super().create(vals)
20 changes: 20 additions & 0 deletions real_estate/models/estate_property_tags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from odoo import models, fields


class EstatePropertyTags(models.Model):
_name = "estate.property.tags"
_description = "Estate Property Tags"
_order = "name"

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

_sql_constraints = [
(
"unique_tag_name",
"UNIQUE(name)",
"This property tag already exits, create a unique one.",
)
]
28 changes: 28 additions & 0 deletions real_estate/models/estate_property_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from odoo import api, fields, models


class EstatePropertyTypes(models.Model):
_name = "estate.property.types"
_description = "Estate Property Types"
_order = "sequence, name"

name = fields.Char(required=True)
sequence = fields.Integer(default=1)
offer_ids = fields.One2many(comodel_name="estate.property.offers", inverse_name="property_type_id")
offer_count = fields.Integer(string="Number of Offers", compute="_compute_offer_count")
property_ids = fields.One2many("estate.property", "estate_property_type_id", string="Properties")

_sql_constraints = [
(
"unique_type_name",
"UNIQUE(name)",
"This property type already exits, create a unique one.",
)
]

@api.depends("offer_ids")
def _compute_offer_count(self):
for record in self:
record.offer_count = len(record.offer_ids)
13 changes: 13 additions & 0 deletions real_estate/models/res_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from odoo import fields, models


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

property_ids = fields.One2many(
comodel_name="estate.property",
inverse_name="seller_id",
domain="[('state', 'in', ('new','offer_received'))]",
)
5 changes: 5 additions & 0 deletions real_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
real_estate.access_estate_property,access_estate_property,real_estate.model_estate_property,base.group_user,1,1,1,1
real_estate.access_estate_property_types,access_estate_property_types,real_estate.model_estate_property_types,base.group_user,1,1,1,1
real_estate.access_estate_property_tags,access_estate_property_tags,real_estate.model_estate_property_tags,base.group_user,1,1,1,1
real_estate.access_estate_property_offers,access_estate_property_offers,real_estate.model_estate_property_offers,base.group_user,1,1,1,1
Loading