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
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
35 changes: 35 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
'name': "Real Estate",

'summary': """
Server framework 101: A New Application"
""",

'description': """
Starting module for "Server framework 101: A New Application"
""",

'author': "Odoo",
'website': "https://www.odoo.com/",
'category': 'Tutorials',
'version': '0.1',
'application': True,
'installable': True,
'depends': ['base'],

'data': [
"data/estate.tag.csv",
"data/estate.property.type.csv",
"data/estate.property.csv",
"security/ir.model.access.csv",
"views/estate_property_views.xml",
"views/estate_property_offer_views.xml",
"views/estate_property_type_views.xml",
"views/estate_tag_views.xml",
"views/estate_menu_views.xml",
"views/res_user_views.xml"
],
'assets': {
},
'license': 'LGPL-3'
}
3 changes: 3 additions & 0 deletions estate/data/estate.property.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"id","name","expecting_price","living_area","total_area","property_type_id:id"
estate_tag_1,"Odoo Farm 2",300000,1900,2000,estate_property_type_1
estate_tag_2,"Odoo LLN",1000000,0,40000,estate_property_type_3
4 changes: 4 additions & 0 deletions estate/data/estate.property.type.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
"id","name","livable"
estate_property_type_1,House,1
estate_property_type_2,Apartment,1
estate_property_type_3,Office,0
3 changes: 3 additions & 0 deletions estate/data/estate.tag.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"id","name","color"
estate_property_1,"Leased",5
estate_property_2,"Empty",8
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_tag
from . import estate_property_type
from . import estate_property_offer
from . import estate_property
from . import res_users
144 changes: 144 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

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


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

name = fields.Char("Name", required=True, translate=True)

description = fields.Char("Description")

stage = fields.Selection([
("new", "New"),
("offer_received", "Offer Received"),
("offer_accepted", "Offer Accepted"),
("sold", "Sold"),
("cancelled", "Cancelled")
], default="new", copy=False)

currency_id = fields.Many2one("res.currency", "Currency")
expecting_price = fields.Monetary("Expecting Price", required=True)
best_offer = fields.Monetary("Best Offer", default=0, compute="_compute_best_offer", store=True)
selling_price = fields.Monetary("Selling Price", default=0, readonly=True)

seller_id = fields.Many2one("res.users", string="Salesperson", index=True, default=lambda self: self.env.user)
buyer_id = fields.Many2one("res.partner", string="Buyer", index=True)

postcode = fields.Integer("Postcode")
bedroom_number = fields.Integer("Bedrooms", default=0)
facade_number = fields.Integer("Facades", default=0)
garage = fields.Boolean("Garage", default=False)
garden = fields.Boolean("Garden", default=False)

living_area = fields.Integer("Living Area (sqm)", default=0)
garden_area = fields.Integer("Garden Area (sqm)", default=0)
total_area = fields.Integer("Total Area (sqm)", default=0, compute="_compute_total_area", inverse="_inverse_total_area", store=True)

garden_orientation = fields.Selection([
("north", "North"),
("south", "South"),
("east", "East"),
("west", "West")
])

def _current_date(self):
return fields.Date.today()

available_from = fields.Date("Date", default=_current_date)

active = fields.Boolean("Active", default=True)
sequence = fields.Integer(default=10)

property_type_id = fields.Many2one("estate.property.type", string="Property Type")
property_livable = fields.Boolean("Livable", compute="_compute_property_livable")

tag_ids = fields.Many2many("estate.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 property in self:
property.total_area = property.living_area + property.garden_area

@api.onchange("total_area")
def _inverse_total_area(self):
for property in self:
temp_total_area = property.total_area
property.living_area = max(0, temp_total_area - property.garden_area)
property.garden_area = temp_total_area - property.living_area

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

@api.depends("property_type_id.livable")
def _compute_property_livable(self):
for property in self:
property.property_livable = property.property_type_id.livable

@api.depends("offer_ids.translated_price")
def _compute_best_offer(self):
for property in self:
property.best_offer = max([0, *property.offer_ids.mapped("translated_price")])

def action_set_as_cancelled(self):
for property in self:
if property.stage in ["sold", "cancelled"]:
raise UserError(_("This property was already set as '%s'", Property.stage._selection[property.stage]))
property.stage = "cancelled"
return True

def action_set_as_sold(self):
for property in self:
if property.stage in ["sold", "cancelled"]:
raise UserError(_("This property was already set as '%s'", Property.stage._selection[property.stage]))
property.stage = "sold"
return True

@api.ondelete(at_uninstall=False)
def _unlink_except_if_advanced_stage(self):
for property in self:
if not property.stage in ["new", "cancelled"]:

Choose a reason for hiding this comment

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

or if property.state not in [....]

raise UserError(_("You cannot delete this property (%s), it is not in a new or cancelled stage.", property.name))

_check_bedroom_number = models.Constraint(
'CHECK(bedroom_number >= 0)',
'The number of bedrooms can\'t be negative.',
)

_check_living_area = models.Constraint(
'CHECK(living_area >= 0)',
'The living area can\'t be negative.',
)

_check_garden_area = models.Constraint(
'CHECK(garden_area >= 0)',
'The garden area can\'t be negative.',
)

_check_total_area = models.Constraint(
'CHECK(total_area >= 0)',
'The total area can\'t be negative.',
)

_check_expected_price = models.Constraint(
'CHECK(expecting_price > 0)',
'The expected price has to be stricly positive'
)

_check_selling_price = models.Constraint(
'CHECK(selling_price >= 0)',
'The selling price has to be stricly positive'
)
96 changes: 96 additions & 0 deletions estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
from odoo import _, api, fields, models
from odoo.exceptions import UserError, ValidationError


class PropertyOffer(models.Model):
_name = "estate.property.offer"
_description = "Offer"
_order = "sequence, id"

property_id = fields.Many2one("estate.property", string="Property", required=True)
partner_id = fields.Many2one("res.partner", string="Partner", index=True, required=True)
property_type_id = fields.Many2one("estate.property.type", string="Property Type", related="property_id.property_type_id")

# Deadline Part

Choose a reason for hiding this comment

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

It is a good practice to leave comments on your code. But consider cleaning them a bit. In this case, you have "deadline part", then a bit after you have "beginning of the deadline part" and then "end of the deadline part". In most of the cases, commenting only the beginning of a block is enough :)

def _current_date(self):
return fields.Date.today()

def _seven_days_from_now_date(self):
return fields.Date.add(fields.Date.today(), days=7)

deadline = fields.Date("Deadline", default=_seven_days_from_now_date)
creation_date = fields.Date("Creation Date", default=_current_date)
validity = fields.Integer("Validity (days)", store=True, compute="_compute_validity", inverse="_inverse_validity")

# Currency Part
currency_id = fields.Many2one("res.currency", "Currency")
property_currency_id = fields.Many2one("res.currency", "Partner Currency", related="property_id.currency_id")
price = fields.Monetary("Original Price", required=True)
translated_price = fields.Monetary("Price", store=True, compute="_compute_translated_price")

# State / validation part
status = fields.Selection([
("accepted", "Accepted"),
("refused", "Refused"),
], copy=False)

sequence = fields.Integer("Sequence", default=0)

# Deadline part
@api.depends("deadline")
def _compute_validity(self):
for offer in self:
offer.validity = (offer.deadline - offer.creation_date).days

# Reverse from _compute_validity, with real-time update because otherwise it's only after closing the form
@api.onchange("validity")
def _inverse_validity(self):
for offer in self:
offer.deadline = fields.Date.add(offer.creation_date, days=offer.validity)

# Currency part

# Translate currency to the one of the property so it's easier to compare
# Also, the webpage doesn't like showing multiple currency signs (as $ and €),
# so we put everything in the base currency for display

def _compute_currency(self):
if self.property_currency_id == self.currency_id:
return self.price
return self.currency_id._convert(self.price, self.property_currency_id)

@api.depends("property_currency_id", "price", "currency_id")
def _compute_translated_price(self):
for offer in self:
offer.translated_price = offer._compute_currency()

# Validation part
def action_confirm(self):
for offer in self:
if offer.property_id.stage in ["offer_accepted", "sold", "cancelled"]:
raise UserError(_("You can't accept new offers"))
offer.property_id.selling_price = offer.translated_price
offer.property_id.buyer_id = offer.partner_id
offer.property_id.stage = "offer_accepted"
offer.status = "accepted"

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

@api.model_create_multi
def create(self, vals_list):
for val in vals_list:
property = self.env["estate.property"].browse(val["property_id"])
if property.stage == "new":
property.stage = "offer_received"

if property.stage != "offer_received":
raise ValidationError(_("You can't create offers at this point"))

return super().create(vals_list)

_check_price = models.Constraint(
'CHECK(price > 0)',
'The price has to be stricly positive'
)
20 changes: 20 additions & 0 deletions estate/models/estate_property_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from odoo import api, fields, models


class PropertyType(models.Model):
_name = "estate.property.type"
_description = "Property Type"
_order = "name"

name = fields.Char("Name", required=True, translate=True)
livable = fields.Boolean("Livable", default=True)

property_ids = fields.One2many("estate.property", "property_type_id", string="Properties")
offer_ids = fields.One2many("estate.property.offer", "property_type_id", string="Property Offers")

offer_count = fields.Integer("Offer Count", compute="_compute_offer_count")

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

from odoo import fields, models

from random import randint


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

def _default_color(self):
return randint(1, 11)

name = fields.Char("Name", required=True, translate=True)
color = fields.Integer(
string='Color Index', default=lambda self: self._default_color(),
help='Tag color. No color means no display in kanban or front-end, to distinguish internal tags from public categorization tags.')

_unique_name = models.Constraint(
'UNIQUE(name)',
'The tag name has to be unique',
)
8 changes: 8 additions & 0 deletions estate/models/res_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from odoo import fields, models

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

# Domain doesn't work
property_ids = fields.One2many("estate.property", "seller_id", string="Properties List",
domain="[('available_from', '<=', 'today'), ('stage', '!=', 'cancelled'), ('stage', '!=', 'sold')]")

Choose a reason for hiding this comment

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

Domain doesn't work because it is supposed to be a list, not a string :D
Also consider doing ('stage', 'not in', ('cancelled', 'sold'))

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_offer,access_estate_property_offer,model_estate_property_offer,base.group_user,1,1,1,1
access_estate_tag,access_estate_tag,model_estate_tag,base.group_user,1,1,1,1
Binary file added estate/static/description/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 34 additions & 0 deletions estate/views/estate_menu_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<menuitem id="estate_menu_root"
name="Real Estate"
web_icon="estate,static/description/icon.png"
sequence="99"
>
<menuitem id="estate_menu_properties"
name="Properties"
sequence="10"
>
<menuitem id="estate_menu_property"
name="Properties"
sequence="10"
action="estate.estate_property_action"
/>
<menuitem id="estate_menu_property_offer"
name="Offers"
sequence="20"
action="estate.estate_property_offer_action"
/>
</menuitem>
<menuitem id="estate_menu_config"
name="Configuration"
sequence="20"
>
<menuitem id="estate_menu_property_type"
name="Property Types"
sequence="20"
action="estate.estate_property_type_action"
/>
</menuitem>
</menuitem>
</odoo>
Loading