Skip to content
Open
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ __pycache__/
# C extensions
*.so

# IDE
.vscode

# Distribution / packaging
.Python
build/
Expand Down
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
14 changes: 14 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "Real Estate",
"application": True,
"depends": ["base"],
"data": [
"security/ir.model.access.csv",
"views/estate_property_views.xml",
"views/estate_property_type_views.xml",
"views/estate_property_tag_views.xml",
"views/estate_property_offer_views.xml",
"views/estate_menus.xml",
"views/res_users_views.xml",
],
}
1 change: 1 addition & 0 deletions estate/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import estate_property, estate_property_type, estate_property_tag, estate_property_offer, res_users
122 changes: 122 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
from odoo import models, fields, api
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 = "Estate Property"
_order = "id desc"

name = fields.Char(string="Title", required=True)
description = fields.Text()
postcode = fields.Char()
date_availability = fields.Date(
string="Available From",
copy=False,
default=fields.Date.today() + relativedelta(days=3),
)
expected_price = fields.Float(required=True)
selling_price = fields.Float(readonly=True, copy=False)
bedrooms = fields.Integer(default=2)
living_area = fields.Integer(string="Living Area (sqm)")
facades = fields.Integer()
garages = fields.Boolean()
garden = fields.Boolean()
garden_area = fields.Integer(string="Garden Area (sqm)")
garden_orientation = fields.Selection(
selection=[
("north", "North"),
("south", "South"),
("east", "East"),
("west", "West"),
]
)
active = fields.Boolean(default=True)
state = fields.Selection(
readonly=False,
selection=[
("new", "New"),
("offer_received", "Offer Received"),
("offer_accepted", "Offer Accepted"),
("sold", "Sold"),
("canceled", "Canceled"),
],
string="Status",
default="new",
required=True,
)
type_id = fields.Many2one("estate.property.type", string="Property Type")
buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False, readonly=True)
salesperson_id = fields.Many2one(
"res.users", string="Salesman", 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")
total_area = fields.Integer(compute="_compute_total_area")
best_price = fields.Float(compute="_compute_best_price")

_check_expected_price = models.Constraint(
"CHECK(expected_price > 0)",
"A property expected price should be strictly positive.",
)

_check_selling_price = models.Constraint(
"CHECK(selling_price >= 0)", "A property selling price should 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("offer_ids.price")
def _compute_best_price(self):
best = max(self.offer_ids.mapped("price")) if self.offer_ids else 0
for record in self:
record.best_price = best

@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 = None

def action_set_sold(self):
for record in self:
if record.state == "canceled":
raise UserError("Canceled properties cannont be sold.")
record.state = "sold"
return True

def action_set_canceled(self):
for record in self:
if record.state == "sold":
raise UserError("Sold properties cannont be canceled.")
record.state = "canceled"
return True

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

@api.ondelete(at_uninstall=False)
def _unlink_except_new_or_canceled(self):
if any(record.state == "new" or record.state == "canceled" for record in self):
raise UserError("Can't delete a new or canceled property.")
81 changes: 81 additions & 0 deletions estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
from odoo import models, fields, api
from dateutil.relativedelta import relativedelta
from odoo.exceptions import UserError


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

price = fields.Float()
status = fields.Selection(
selection=[("accepted", "Accepted"), ("refused", "Refused")], copy=False
)
partner_id = fields.Many2one("res.partner", required=True)
property_id = fields.Many2one("estate.property", required=True)
validity = fields.Integer(default=7, string="Validity (days)")
date_deadline = fields.Date(
compute="_compute_date_deadline",
inverse="_inverse_date_deadline",
string="Deadline",
)
property_type_id = fields.Many2one(
string="Property Type", related="property_id.type_id", store=True
)

_check_price = models.Constraint(
"CHECK(price > 0)", "An offer price should be strictly positive."
)

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

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

def action_set_accepted(self):
states = self.property_id.offer_ids.mapped("status")
for record in self:
if "accepted" in states:
raise UserError("Une offre a déjà été acceptée.")
record.status = "accepted"
record.property_id.buyer_id = self.partner_id
record.property_id.selling_price = self.price
record.property_id.state = "offer_accepted"
return True

def action_set_refused(self):
for record in self:
previous_status = record.status
record.status = "refused"
if previous_status == "accepted":
record.property_id.buyer_id = None
record.property_id.selling_price = 0
return True

@api.model
def create(self, vals_list):
for vals in vals_list:
prop = self.env["estate.property"].browse(vals["property_id"])
best_price = prop.best_price
if vals["price"] < best_price:
raise UserError(
"You can't create an offer with a lower amount than an existing offer."
)
prop.state = "offer_received"
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 models, fields


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

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

_check_unique_name = models.Constraint(
"UNIQUE(name)", "A property tag name should be unique."
)
25 changes: 25 additions & 0 deletions estate/models/estate_property_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from odoo import models, fields, api


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

name = fields.Char(required=True)
property_ids = fields.One2many("estate.property", "type_id", string="Properties")
sequence = fields.Integer()
offer_ids = fields.One2many(
"estate.property.offer", "property_type_id", string="Offers"
)
offer_count = fields.Integer(compute="_compute_offer_count")

_check_unique_name = models.Constraint(
"UNIQUE(name)", "A property tag name should be unique."
)

@api.depends("offer_ids")
def _compute_offer_count(self):
for record in self:
record.offer_count = len(record.offer_ids)
return True
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 ResUsers(models.Model):
_inherit = "res.users"

property_ids = fields.One2many(
"estate.property",
"salesperson_id",
domain="['|',('state', '=', 'new'),('state', '=', 'offer_received')]",
string="Properties",
)
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,access_estate_property,model_estate_property,base.group_user,1,1,1,1
access_estate_property_type,access_estate_property_type,model_estate_property_type,base.group_user,1,1,1,1
access_estate_property_tag,access_estate_property_tag,model_estate_property_tag,base.group_user,1,1,1,1
access_estate_property_offer,access_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_property_menu_root" name="Real Estate">
<menuitem id="estate_property_menu" name="Advertisements">
<menuitem id="estate_property_menu_action" action="estate_property_action"/>
</menuitem>
<menuitem id="estate_settings_menu" 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="Property Type">
<sheet>
<group>
<field name="price"/>
<field name="partner_id"/>
<field name="validity"/>
<field name="date_deadline"/>
<field name="status"/>
</group>
</sheet>
</form>
</field>
</record>

<record id="estate_property_offer_view_tree" 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="Offer" editable="top" decoration-danger="status == 'refused'" decoration-success="status == 'accepted'">
<field name="price"/>
<field name="partner_id"/>
<field name="validity"/>
<field name="date_deadline"/>
<button name="action_set_accepted" string="Accept" type="object" icon="fa-check" invisible="status"/>
<button name="action_set_refused" string="Refuse" type="object" icon="fa-times" 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>
35 changes: 35 additions & 0 deletions estate/views/estate_property_tag_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?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="Property Tag">
<sheet>
<div class="oe_title">
<h1><field name="name"/></h1>
</div>
<group>
<field name="color" widget="color_picker"/>
</group>
</sheet>
</form>
</field>
</record>

<record id="estate_property_tag_view_tree" 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 string="Tag" editable="top">
<field name="name"/>
</list>
</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