Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
24 changes: 24 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "Real Estate",
"description": "",
"depends": ["base"],
"sequence": 1,
"data": [
"security/ir.model.access.csv",
"data/ir_cron_data.xml",
"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_menus.xml",
"views/res_users_views.xml",
],
'assets': {
'web.assets_backend': [
'estate/static/src/css/custom.css',
],
},
"license": "LGPL-3",
"application": True,
"installable": True,
}
15 changes: 15 additions & 0 deletions estate/data/ir_cron_data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="ir_cron_estate_property_offer_expire" model="ir.cron">
<field name="name">Expire Property Offers</field>
<field name="model_id" ref="model_estate_property_offer"></field>
<field name="state">code</field>
<field name="code">model._cron_refuse_expired_offers()</field>
<field name="active" eval="True"/>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="nextcall">2025-08-26 18:30:00</field>
</record>
</data>
</odoo>
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 estate_property
from . import estate_property_type
from . import estate_property_tag
from . import estate_property_offer
from . import res_users
156 changes: 156 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
from datetime import timedelta
from odoo import models, fields, api
from odoo.exceptions import ValidationError, UserError
from odoo.tools import float_is_zero, float_compare


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

_sql_constraints = [
(
"check_expected_price",
"CHECK(expected_price > 0)",
"The expected price must be greater than 0.",
),
]

name = fields.Char(string="Name", required=True)
description = fields.Text(string="Description")
active = fields.Boolean(
string="Active",
default=True,
help="Mark as active if you want the property to be listed.",
)
postcode = fields.Char(string="Postcode")
property_type_id = fields.Many2one("estate.property.type", string="Property Type")
date_availability = fields.Date(
string="Available From",
default=fields.Date.today() + timedelta(days=90),
copy=False,
)
expected_price = fields.Float(string="Expected Price", required=True)
selling_price = fields.Float(string="Selling Price", readonly=True, copy=False)
bedrooms = fields.Integer(string="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)")
garden_orientation = fields.Selection(
string="Garden Orientation",
selection=[
("north", "North"),
("south", "South"),
("east", "East"),
("west", "West"),
],
)
total_area = fields.Integer(
string="Total Area (sqm)",
compute="_compute_total_area",
)
state = fields.Selection(
string="State",
required=True,
default="new",
copy=False,
selection=[
("new", "New"),
("offer_received", "Offer Received"),
("offer_accepted", "Offer Accepted"),
("sold", "Sold"),
("cancelled", "Cancelled"),
],
)
salesman_id = fields.Many2one(
"res.users", string="Salesman", default=lambda self: self.env.user
)
buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False, readonly=True)
tag_ids = fields.Many2many(
"estate.property.tag",
string="Tags",
help="Properties associated with this tag.",
)
offer_ids = fields.One2many(
"estate.property.offer",
"property_id",
string="Offers",
help="Offers made on this property.",
)
best_price = fields.Float(
string="Best Offer",
compute="_compute_best_price",
)

@api.depends("living_area", "garden_area", "garden")
def _compute_total_area(self):
for property in self:
property.total_area = property.living_area + (
property.garden_area if property.garden else 0
)

@api.depends("offer_ids.price")
def _compute_best_price(self):
for property in self:
property.best_price = max(property.offer_ids.mapped("price"), default=0.0)

@api.constrains("selling_price", "expected_price")
def _check_selling_price(self):
for property in self:
if float_is_zero(property.selling_price, precision_rounding=2):
continue

if (
float_compare(
property.selling_price,
property.expected_price * 0.9,
precision_rounding=2,
)
< 0
):
raise ValidationError(
"The selling price cannot be lower than 90'%' of the expected price!"
)

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

@api.ondelete(at_uninstall=False)
def _unlink_check(self):
for property in self:
if property.state not in ["new", "cancelled"]:
raise UserError(
"You cannot delete a property that is not new or cancelled."
)

def action_set_sold(self):
for property in self:
if property.selling_price > 0.0 and property.state != "cancelled":
property.state = "sold"
elif property.state == "cancelled":
raise UserError("A cancelled property cannot be sold.")
elif property.state == "new" or property.state == "offer_received":
raise UserError(
"This property must have an accepted offer before it can be sold."
)
elif property.state == "sold":
raise UserError("This property is already sold.")

def action_set_cancelled(self):
for property in self:
if property.state != "cancelled" and property.state != "sold":
property.state = "cancelled"
elif property.state == "cancelled":
raise UserError("This property is already cancelled.")
elif property.state == "sold":
raise UserError("A sold property cannot be cancelled.")
114 changes: 114 additions & 0 deletions estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
from datetime import timedelta
from odoo import models, fields, api
from odoo.exceptions import UserError


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

_sql_constraints = [
("check_price", "CHECK(price > 0)", "The price must be greater than 0."),
]

price = fields.Float(string="Price")
status = fields.Selection(
string="Status",
selection=[
("accepted", "Accepted"),
("refused", "Refused"),
],
)
validity = fields.Integer(
string="Validity(days)",
default=7,
help="Validity of the offer in days, after that it will be refused automatically.",
)
date_deadline = fields.Date(
string="Deadline",
compute="_compute_date_deadline",
inverse="_inverse_date_deadline",
store=True,
)
partner_id = fields.Many2one("res.partner", string="Partner", required=True)
property_id = fields.Many2one("estate.property", string="Property", required=True)
property_type_id = fields.Many2one(
"estate.property.type",
string="Property Type",
related="property_id.property_type_id",
store=True,
)

@api.depends("validity")
def _compute_date_deadline(self):
for offer in self:
base_date = (
fields.Date.to_date(offer.create_date)
if offer.create_date
else fields.Date.context_today(offer)
)
offer.date_deadline = base_date + timedelta(days=offer.validity or 0)

def _inverse_date_deadline(self):
for offer in self:
if offer.date_deadline:
base_date = (
fields.Date.to_date(offer.create_date)
if offer.create_date
else fields.Date.context_today(offer)
)
offer.validity = (offer.date_deadline - base_date).days
else:
offer.validity = 0

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

if record.get("price") < property.best_price:
raise UserError(
"You cannot create an offer lower than an existing one."
)

property.state = "offer_received"
return super().create(vals_list)

@api.model
def _cron_refuse_expired_offers(self):
today = fields.Date.context_today(self)
expired_offers = self.search(
[
("status", "!=", "accepted"),
("date_deadline", "<=", today),
]
)
expired_offers.write({"status": "refused"})

def action_set_accepted(self):
for offer in self:
if offer.property_id.selling_price == 0.0:
offer.write({"status": "accepted"})

offer.property_id.write(
{
"state": "offer_accepted",
"selling_price": offer.price,
"buyer_id": offer.partner_id.id,
}
)

other_offers = offer.property_id.offer_ids - offer

other_offers.write({"status": "refused"})
else:
raise UserError("One offer is already accepted for this property.")

def action_set_refused(self):
for offer in self:
if offer.status == "accepted":
offer.property_id.state = "offer_received"
offer.property_id.buyer_id = False
offer.property_id.selling_price = 0.0
offer.status = "refused"
15 changes: 15 additions & 0 deletions estate/models/estate_property_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from odoo import models, fields


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

_sql_constraints = [

Choose a reason for hiding this comment

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

You can define it after the field declaration.

Choose a reason for hiding this comment

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

can you please adapt this changes ?

("unique_name", "UNIQUE(name)", "A tag with same name is already exists."),
]

name = fields.Char(string="Tag Name", required=True)
description = fields.Text(string="Description")
color = fields.Integer(string="Color")
28 changes: 28 additions & 0 deletions estate/models/estate_property_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from odoo import models, fields, api


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

_sql_constraints = [
("unique_name", "UNIQUE(name)", "A type with same name is already exists."),
]

name = fields.Char(required=True)
property_ids = fields.One2many(
comodel_name="estate.property",
inverse_name="property_type_id",
string="Properties",
)
offer_ids = fields.One2many("estate.property.offer", "property_type_id")
sequence = fields.Integer(
"Sequence", default=1, help="Used to order types. Lower is better."
)
offer_count = fields.Integer("Offers", compute="_compute_offer_count")

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


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

property_ids = fields.One2many(
"estate.property",
"salesman_id",
string="Properties",
domain=[("state", "in", ["new", "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_estate_property,estate.property,model_estate_property,base.group_user,1,1,1,1
access_estate_property_type,estate.property.type,model_estate_property_type,base.group_user,1,1,1,1
access_estate_property_tag,estate.property.tag,model_estate_property_tag,base.group_user,1,1,1,1
access_estate_property_offer,estate.property.offer,model_estate_property_offer,base.group_user,1,1,1,1
12 changes: 12 additions & 0 deletions estate/static/src/css/custom.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* Apply only inside this specific list view */
/* Left-align the price column cells */
.o_list_view.price-left-align td.o_list_number[name="price"] {
text-align: left !important;
}

/* Left-align the price column header */
.o_list_view.price-left-align th[data-name="price"],
.o_list_view.price-left-align th[data-name="price"] .o_list_number_th {
text-align: left !important;
justify-content: flex-start !important; /* override flex alignment */
}
Loading