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
2 changes: 2 additions & 0 deletions estate/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

from . import models
17 changes: 17 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
'name': 'Estate',
'category': 'Sales',
'sequence': 1,
'summary': 'Sell and bid on the hottest real estate properties.',

Choose a reason for hiding this comment

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

You could explain that in your pr message for instance

'website': 'https://www.odoo.com/app/estate',
'depends': [
'base_setup',

Choose a reason for hiding this comment

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

Shouldn't you use base here ?

'web',
],
'data': [
'security/ir.model.access.csv',
'views/estate_property_views.xml',
'views/estate_menus.xml',
],
'application': True
}
6 changes: 6 additions & 0 deletions estate/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

from . import estate_property
from . import estate_property_type
from . import estate_property_tag
from . import estate_property_offer
from . import inherited_model
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 odoo import fields, models, api
from dateutil.relativedelta import relativedelta
from odoo.exceptions import UserError


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

name = fields.Char("Title", required=True, translate=True)
property_type_id = fields.Many2one(
'estate.property.type',
string="Property Type",
)
postcode = fields.Char("Postcode", required=True)
availability = fields.Date(
"Available From",
required=True,
copy=False,
default=lambda self: fields.Date.today() + relativedelta(months=3),
)
description = fields.Text("Description")
bedrooms = fields.Integer("Bedrooms", required=True, default=2)
living_area = fields.Integer("Living Area (sqm)", required=True)
currency_id = fields.Many2one('res.currency', string="Currency", default=lambda self: self.env.company.currency_id.id)
expected_price = fields.Monetary("Expected Price", required=True)
selling_price = fields.Monetary("Selling Price", readonly=True, copy=False)
# best_offer_id = fields.Many2one('estate.property.offer', string='Best Offer', readonly=True)
facades = fields.Integer("Facades", default=False)
garage = fields.Boolean("Garage", default=False)
garden = fields.Boolean("Garden", default=False)
garden_area = fields.Integer("Garden Area (sqm)", required=False)
garden_orientation = fields.Selection(selection=[('north', "North"), ('south', "South"), ('east', "East"), ('west', "West")], string="Garden Orientation")

@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

total_area = fields.Integer("Total Area (sqm)", compute='_compute_area')

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

state = fields.Selection(
string="Status",
selection=[('new', "New"), ('offer_received', "Offer Received"), ('offer_accepted', "Offer Accepted"), ('sold', "Sold"), ('canceled', "Canceled")],
required=True,
copy=False,
default='new',
)
Comment on lines +52 to +58

Choose a reason for hiding this comment

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

Don't split your fields definitions. You should move this up with the rest of them 😄


def sold_action(self):
for record in self:

Choose a reason for hiding this comment

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

Try to use a meaningful name instead of record -> property

if record.state != 'canceled':
record.state = 'sold'
else:
raise UserError(record.env._("You can not sell a canceled property."))

Choose a reason for hiding this comment

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

FYI here you could use self.env._ but that's good also 😄

Comment on lines +62 to +65

Choose a reason for hiding this comment

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

Suggested change
if record.state != 'canceled':
record.state = 'sold'
else:
raise UserError(record.env._("You can not sell a canceled property."))
if record.state == 'canceled':
raise UserError(record.env._("You can not sell a canceled property."))
record.state = 'sold'

return True

def cancel_action(self):
for record in self:
if record.state != 'sold':
record.state = 'canceled'
else:
raise UserError(record.env._("You can not cancel a sold property."))
Comment on lines +70 to +73

Choose a reason for hiding this comment

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

Suggested change
if record.state != 'sold':
record.state = 'canceled'
else:
raise UserError(record.env._("You can not cancel a sold property."))
if record.state == 'sold':
raise UserError(record.env._("You can not cancel a sold property."))
record.state = 'canceled'

return True
active = fields.Boolean("Active", default=True)
buyer_id = fields.Many2one('res.partner', string="Buyer", copy=False)
seller_id = fields.Many2one('res.users', string="Salesperson", 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",
)
best_offer = fields.Monetary(
string="Best Offer",
compute='_compute_best_offer',
)
Comment on lines +75 to +90

Choose a reason for hiding this comment

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

Here too, the format should be fields/constraints > create/write/unlink/compute/onchange > the rest.


@api.depends('offer_ids.price')
def _compute_best_offer(self):
for record in self:
if record.offer_ids:
record.best_offer = max(record.offer_ids.mapped('price'))
else:
record.best_offer = 0.0

def write(self, vals):
result = super().write(vals)
if 'offer_ids' in vals:
for record in self:
if record.offer_ids and record.state == 'new':
record.state = 'offer_received'
elif not record.offer_ids and record.state == 'offer_received':
record.state = 'new'
return result

_check_expected_price = models.Constraint(
'CHECK(expected_price > 0)',
"The expected price should be higher than zero.",
)
_check_selling_price = models.Constraint(
'CHECK(selling_price >= 0)',
"The selling price can't be negative",
)

@api.ondelete(at_uninstall=False)
def _unlink_if_new_or_canceled(self):

Choose a reason for hiding this comment

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

Here what you are unlinking is a property so it is better to name it _unlink_property 😄

if any((not (record.state == 'new') and not (record.state == 'canceled'))
for record in self):
raise UserError("Only new and canceled properties can be deleted!")
77 changes: 77 additions & 0 deletions estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
from odoo import models, fields, api
from dateutil.relativedelta import relativedelta
from odoo.exceptions import UserError, ValidationError


class EstatePropertyOffer(models.Model):
_name = 'estate.property.offer'
_description = "Offer to buy real estate property"
_order = 'price desc'

property_id = fields.Many2one(
'estate.property',
string="Property Name",
required=True,
ondelete='cascade',
)
property_type_id = fields.Many2one(related="property_id.property_type_id", store=True)
partner_id = fields.Many2one('res.partner', string="Partner", required=True)
create_date = fields.Date(default=lambda self: fields.Date.today())
validity = fields.Integer("Validity (days)", default=7)
date_deadline = fields.Date(
"Deadline",
compute='_compute_date',
inverse='_inverse_date',
)

@api.depends('validity')
def _compute_date(self):
for record in self:
record.date_deadline = record.create_date + relativedelta(days=record.validity)

def _inverse_date(self):
for record in self:
record.validity = (record.date_deadline - record.create_date).days

currency_id = fields.Many2one('res.currency', string="Currency", default=lambda self: self.env.company.currency_id.id)
price = fields.Monetary("Price")
status = fields.Selection(
string="Status",
selection=[('accepted', "Accepted"), ('refused', "Refused")],
copy=False,
)

def accept_offer(self):
for record in self:
if record.property_id.offer_ids.filtered(lambda o: o.status == 'accepted'):
raise UserError(record.env._("There is already an accepted offer for this property."))
record.status = 'accepted'
record.property_id.selling_price = record.price
record.property_id.state = 'offer_accepted'
record.property_id.buyer_id = record.partner_id
return True

def refuse_offer(self):
for record in self:
record.status = 'refused'
return True

_check_price = models.Constraint(
'CHECK(price > 0)',
"The offer price must be positive",
)

@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if 'property_id' in vals and 'price' in vals:

Choose a reason for hiding this comment

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

Shouldn't you make property_id and price required ?

linked_property = self.env['estate.property'].browse(vals['property_id'])

Choose a reason for hiding this comment

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

don't browse in a loop, try to do it outside the loop so it is more efficient otherwise you would have to query record one by one instead of all of them at once

if linked_property.best_offer and vals['price'] < linked_property.best_offer:

Choose a reason for hiding this comment

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

Here too use the currency compare or float_compare 😄

raise UserError("The offer price must be higher than the current best offer of %.2f" % property.best_offer)
return super().create(vals_list)

@api.constrains('status')
def _check_fair_price(self):
for record in self:
if record.status == 'accepted' and record.price < record.property_id.expected_price * 0.9:

Choose a reason for hiding this comment

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

don't use normal comparison operators against floats. Here as this is a monetary field, you can use record.currency_id.compare(record.price, record.property_id.expected_price * 0.9) < 0

raise ValidationError(record.env._(f"The selling price must be at least {90}% of the expected price. \n If you want to accept this offer, lower the expected price."))
19 changes: 19 additions & 0 deletions estate/models/estate_property_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from odoo import fields, models


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

name = fields.Char(
"Name",
required=True,
)
_name_uniq = models.Constraint(
'unique(name)',
'This property tag already exists.',

Choose a reason for hiding this comment

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

Double quotes here 😄
Also don't forget about the splitting, try to put fields together

)
color = fields.Integer(
"Color"
)
31 changes: 31 additions & 0 deletions estate/models/estate_property_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from odoo import fields, models, api


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

name = fields.Char(
"Name",
required=True,
)
_name_uniq = models.Constraint(
'unique(name)',
'This property type already exists.',
)
property_ids = fields.One2many(
'estate.property',
'property_type_id',
)
sequence = fields.Integer("Sequence", default=1)
offer_ids = fields.One2many(
'estate.property.offer',
'property_type_id',
)
offer_count = fields.Integer(compute='_compute_offer_count')

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


class Users(models.Model):
_inherit = 'res.users'

property_ids = fields.One2many('estate.property', 'seller_id', 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/views/estate_menus.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<menuitem id="estate_menu_root" name="Estate">
<menuitem id="estate_first_level_menu" name="Advertisements">
<menuitem id="estate_property_menu_action" action="estate_property_action"/>
</menuitem>
<menuitem id="estate_second_level_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>
Loading