Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 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
21 changes: 21 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
'name': 'Odoo Tutorial Real Estate',
'category': 'Real Estate',
'version': '19.0.1.0',
'author': 'Hazei',
'summary': 'Real Estate Management Tutorial',
'depends': [
'base',
],
'data': [
'security/ir.model.access.csv',
'views/estate_property_offer_view.xml',
'views/estate_property_tags_view.xml',
'views/estate_property_type.xml',
'views/estate_property_views.xml',
'views/res_users_views.xml',
'views/menu_views.xml',
],
'application': True,
'license': 'LGPL-3',
}
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_offer
from . import estate_property_tags
from . import estate_property_type
from . import res_users
123 changes: 123 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
from datetime import date

from dateutil.relativedelta import relativedelta

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


class EstateProperty(models.Model):
# ---------------------------------------- Private Attributes ---------------------------------

_name = "estate.property"
_description = "Real Estate Property"
_order = "id desc"

_check_expected_price = models.Constraint(
'CHECK(expected_price >= 0)', 'The expected price must be strictly positive.')

_check_selling_price = models.Constraint(
'CHECK(selling_price > 0)', 'The selling price must be positive.')

# ---------------------------------------- Fields Declaration ---------------------------------

name = fields.Char(string="Title", required=True)
property_type = fields.Selection(
string='Property Type',
selection=[('house', 'House'), ('apartment', 'Apartment')])
description = fields.Text()
postcode = fields.Char()
date_availability = fields.Date(copy=False, readonly=True, default=lambda self: date.today() + relativedelta(months=3))
expected_price = fields.Float("Expected Price", required=True)
selling_price = fields.Float("Selling Price", readonly=True, copy=False)
bedrooms = fields.Integer(default=2)
living_area = fields.Integer(string='Living Area (m2)')
facades = fields.Integer()
garage = fields.Boolean()
garden = fields.Boolean()
garden_area = fields.Integer(string='Garden Area (m2)')
garden_orientation = fields.Selection(
string='Garden Orientation',
selection=[('north', 'North'), ('south', 'South'),
('east', 'East'), ('west', 'West')])
active = fields.Boolean(default=True)
state = fields.Selection(
copy=False,
readonly=True,
default='new',
string='Property_States',
selection=[
('new', 'New'),
('offer_received', 'Offer Received'),
('offer_accepted', 'Offer Accepted'),
('sold', 'Sold'),
('cancelled', 'Cancelled')]
)

property_type_id = fields.Many2one('estate.property.type', string='Type')
buyer_id = fields.Many2one('res.partner', string='Buyer', copy=False)
salesman_id = fields.Many2one('res.users', string='Salesman', default=lambda self: self.env.user)
tags_ids = fields.Many2many('estate.property.tags', string='Tags')
offer_ids = fields.One2many("estate.property.offer", "property_id", string="offer")
total_area = fields.Integer(string='Total Area(m2)', compute='_compute_total_area', store=True)
best_price = fields.Float(string='Best Offer', compute='_compute_best_price', store=True)

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

@api.depends('offer_ids.price')
def _compute_best_price(self):
for record in self:
record.best_price = max(record.offer_ids.mapped('price')) if record.offer_ids else 0

# ----------------------------------- Constrains and Onchanges --------------------------------

@api.constrains('selling_price', 'expected_price')
def _check_selling_price_expected_price(self):
for record in self:
if record.selling_price == 0:
continue
if float_compare(record.selling_price, 0.9 * record.expected_price, precision_digits=2) == -1:
raise UserError("The selling price must be at least 90% of the expected price.")

@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.onchange('offer_ids')
def _onchange_offers(self):
for record in self:
has_offers = any(offer.status not in ('refused', 'accepted') for offer in record.offer_ids)
if record.state == 'new' and has_offers:
record.state = 'offer_received'

if not has_offers and not any(offer.status == 'accepted' for offer in record.offer_ids) and record.state in ('offer_received',):
record.state = 'offer_receive'
# ------------------------------------------ CRUD Methods -------------------------------------

@api.ondelete(at_uninstall=False)
def _if_new_or_canceled(self):
if not set(self.mapped("state")) <= {"new", "canceled"}:
raise UserError("Only new and canceled properties can be deleted.")

# ---------------------------------------- Action Methods -------------------------------------

def action_set_sold(self):
if self.state == "cancelled":
raise UserError("A cancelled property can not be sold")
self.state = "sold"
return True

def action_set_cancelled(self):
if self.state == "sold":
raise UserError("A sold property can not be cancelled")
self.state = "cancelled"
return True
82 changes: 82 additions & 0 deletions estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from dateutil.relativedelta import relativedelta

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


class EstatePropertyOffer(models.Model):

# ---------------------------------------- Private Attributes ---------------------------------

_name = 'estate.property.offer'
_description = 'Estate Property Offer'
_order = 'price desc'

# --------------------------------------- Fields Declaration ----------------------------------

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

# ---------------------------------------- Compute methods ------------------------------------

@api.depends('validity', 'create_date')
def _compute_date_deadline(self):
for offer in self:
if not offer.create_date:
offer.date_deadline = fields.Date.today() + relativedelta(days=offer.validity)

# ------------------------------------------ CRUD Methods -------------------------------------

@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if vals.get("property_id") and vals.get("price"):
prop = self.env["estate.property"].browse(vals["property_id"])

if float_compare(vals["price"], prop.expected_price * 0.9, precision_rounding=0.01) < 0:
raise UserError(
"The offer price must be at least 90%% of the expected price.")

if prop.offer_ids:
max_offer = max(prop.mapped("offer_ids.price"))
if float_compare(vals["price"], max_offer, precision_rounding=0.01) <= 0:
raise UserError(
"The offer must be higher than %.2f" % max_offer)

new_offers = super().create(vals_list)

for offer in new_offers:
offer.property_id.state = "offer_received"

return new_offers

# ---------------------------------------- Action Methods -------------------------------------

def action_accept(self):
for offer in self:
if any(prop_offer.status == 'accepted' for prop_offer in offer.property_id.offer_ids):
raise UserError(
"An offer has already been accepted for this property.")

if offer.status == 'refused':
raise UserError("A refused offer cannot be accepted.")

offer.status = 'accepted'
offer.property_id.write({
'state': 'offer_accepted',
'selling_price': offer.price,
'buyer_id': offer.partner_id.id,
})
return True

def action_refuse(self):
if any(offer.status == 'accepted' for offer in self):
raise UserError("An accepted offer cannot be refused.")
self.status = 'refused'
return True
14 changes: 14 additions & 0 deletions estate/models/estate_property_tags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from odoo import models, fields


class PropertyTypeTags(models.Model):

# ---------------------------------------- Private Attributes ---------------------------------
_name = 'estate.property.tags'
_description = 'Estate Property Tags'
_order = 'name'

# --------------------------------------- Fields Declaration ----------------------------------

name = fields.Char(required=True)
color = fields.Integer()
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 api, fields, models


class PropertyType(models.Model):

# ---------------------------------------- Private Attributes ---------------------------------

_name = 'estate.property.type'
_description = 'Estate Property Type'
_order = 'sequence, name'

# --------------------------------------- Fields Declaration ----------------------------------

name = fields.Char(required=True)
property_ids = fields.One2many('estate.property', 'property_type_id', string='Properties')
sequence = fields.Integer(string="Sequence", default=10)
offer_ids = fields.One2many("estate.property.offer", "property_type_id", string="Offers")
offer_count = fields.Integer(string="Offer Count", compute="_compute_offer_count")

# ---------------------------------------- Compute methods ------------------------------------

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


class ResUsers(models.Model):

_inherit = "res.users"

property_ids = fields.One2many(
"estate.property", "salesman_id", string="Available 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,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_tags,access_estate_property_tags,model_estate_property_tags,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
41 changes: 41 additions & 0 deletions estate/views/estate_property_offer_view.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?xml version="1.0"?>
<odoo>
<record id="estate_property_action" model="ir.actions.act_window">
<field name="name">Property</field>
<field name="res_model">estate.property</field>
<field name="view_mode">list,form</field>
</record>
<record id="estate_property_offer_view_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 string="Property Offers" editable="bottom" decoration-danger="status=='refused'" decoration-success="status=='accepted'">
<field name="partner_id"/>
<field name="property_type_id"/>
<field name="price"/>
<field name="status"/>
<field name="validity"/>
<field name="date_deadline"/>
<button name="action_accept" type="object" icon="fa-check" title="Accept" invisible="status in ['accepted', 'refused']"/>
<button name="action_refuse" type="object" icon="fa-times" title="Refuse" invisible="status in ['accepted', 'refused']"/>
</list>
</field>
</record>
<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 Offer">
<sheet>
<group>
<field name="partner_id"/>
<field name="price"/>
<field name="status"/>
<field name="validity"/>
<field name="date_deadline"/>
</group>
</sheet>
</form>
</field>
</record>
</odoo>
29 changes: 29 additions & 0 deletions estate/views/estate_property_tags_view.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<odoo>
<record id="estate_property_tags_action" model="ir.actions.act_window">
<field name="name">Property Tags</field>
<field name="res_model">estate.property.tags</field>
<field name="view_mode">list,form</field>
</record>
<record id="estate_property_tag_view_list" model="ir.ui.view">
<field name="name">estate.property.tag.list</field>
<field name="model">estate.property.tags</field>
<field name="arch" type="xml">
<list string="Property Tags" editable="bottom">
<field name="name"/>
</list>
</field>
</record>
<record id="estate_property_tag_view_form" model="ir.ui.view">
<field name="name">estate.property.tag.form</field>
<field name="model">estate.property.tags</field>
<field name="arch" type="xml">
<form string="Property Tag">
<sheet>
<group>
<field name="name"/>
</group>
</sheet>
</form>
</field>
</record>
</odoo>
Loading