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
19 changes: 19 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "estate",
"depends": [
"base",
],
"application": True,
"author": "Yoelf",
"license": "LGPL-3",
"version": "19.0.0.1.0",
"data": [
"security/ir.model.access.csv",
"views/estate_property_views.xml",
"views/estate_property_offer_views.xml",
"views/estate_property_type_views.xml",
"views/estate_property_tag_views.xml",
"views/res_user_views.xml",
"views/estate_menus.xml",
],
}
Comment thread
yoelfatihi marked this conversation as resolved.
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_user
128 changes: 128 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
from odoo.exceptions import UserError, ValidationError
from odoo.tools.float_utils import float_compare, float_is_zero

from odoo import api, fields, models


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

name = fields.Char(required=True)
Comment thread
yoelfatihi marked this conversation as resolved.
description = fields.Text()
postcode = fields.Char()
date_availability = fields.Date(
copy=False, default=lambda self: fields.Date.add(fields.Date.today(), months=3)
)
Comment thread
yoelfatihi marked this conversation as resolved.
expected_price = fields.Integer(required=True)
selling_price = fields.Integer(readonly=True, copy=False)
bedrooms = fields.Integer(default=2)
living_area = fields.Integer(default=0)
facades = fields.Integer()
garage = fields.Boolean()
garden = fields.Boolean()
garden_area = fields.Integer(default=0)
garden_orientation = fields.Selection(
string="orientation",
selection=[
("north", "North"),
("south", "South"),
("east", "East"),
("west", "West"),
],
)
active = fields.Boolean(default=True)
state = fields.Selection(
string="State",
selection=[
("new", "New"),
("offer_received", "Offer Received"),
("offer_accepted", "Offer Accepted"),
("sold", "Sold"),
("cancelled", "Cancelled"),
],
required=True,
default="new",
)
estate_property_type_id = fields.Many2one(
"estate.property.type", string="Property Type"
)
Comment on lines +48 to +50
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

If you name the field 'property_type_id' you won't need to add the string attribute :)

buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False, readonly=True)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Won't say it anymore ater that but there are more instances:

For many2one field the default string will be Xyz for xyz_id
And for one2many and many2many Xyz for xyz_ids

salesperson_id = fields.Many2one(
"res.users", string="Salesman", default=lambda self: self.env.user
)
Comment thread
yoelfatihi marked this conversation as resolved.
estate_property_tag_ids = fields.Many2many("estate.property.tag", string="Tags")
estate_property_offer_ids = fields.One2many("estate.property.offer", "property_id")
total_area = fields.Integer(compute="_compute_total_area")
best_price = fields.Float(compute="_compute_best_price")

_check_expected_price_positive = models.Constraint(
"CHECK(expected_price > 0)",
"A property expected price must be strictly positive.",
)
_check_selling_price_positive = models.Constraint(
"CHECK(selling_price > 0)", "A property selling price must be positive"
)

@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 property.living_area
)

@api.depends("estate_property_offer_ids.price")
def _compute_best_price(self):
for property in self:
property.best_price = (
max(property.estate_property_offer_ids.mapped("price"))
if property.estate_property_offer_ids
else None
)
Comment on lines +80 to +84
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
property.best_price = (
max(property.estate_property_offer_ids.mapped("price"))
if property.estate_property_offer_ids
else None
)
property.best_price = max(property.estate_property_offer_ids.mapped("price"), default=0)


@api.onchange("garden")
def _onchange_has_garden(self):
if self.garden:
self.garden_area = 10
self.garden_orientation = "north"
Comment on lines +86 to +90
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Here you forgot to reset to the default value if garden is false


def action_sell(self):
for property in self:
if property.state == "cancelled":
raise UserError("You can't sell a a cancelled property :)")

property.state = "sold"
return True
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

return True is indented too much here, will return at the first iteration of the loop


def action_cancel(self):
for property in self:
if property.state == "sold":
raise UserError("you can't cancel a sold property :)")

property.state = "cancelled"
return True
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same


@api.constrains("selling_price", "expected_price")
def _check_expected_to_selling(self):
for property in self:
if (
not float_is_zero(property.selling_price, precision_digits=2)
) and float_compare(
property.selling_price,
property.expected_price * 0.9,
precision_digits=2,
) == -1:
raise ValidationError(
r"the selling price cannot be lower than 90% of the expected price"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why a raw string here ?

)

@api.ondelete(at_uninstall=False)
def on_delete(self):
for property in self:
if property.state not in ["new", "cancelled"]:
raise UserError(
f"You cannot delete a property at the {property.state} state"
)
88 changes: 88 additions & 0 deletions estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
from odoo.exceptions import UserError

from odoo import api, fields, models


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

price = fields.Float()
state = fields.Selection(
string="Status",
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)
date_deadline = fields.Date(
compute="_compute_date_deadline", inverse="_inverse_date_deadline"
)
property_type_id = fields.Many2one(
"estate.property.type",
related="property_id.estate_property_type_id",
store=True,
)

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

@api.depends("validity")
def _compute_date_deadline(self):
for offer in self:
if offer.create_date:
offer.date_deadline = fields.Date.add(
offer.create_date,
days=offer.validity,
)
else:
offer.date_deadline = fields.Date.add(
fields.Date.today(), days=offer.validity
)
Comment on lines +37 to +45
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
if offer.create_date:
offer.date_deadline = fields.Date.add(
offer.create_date,
days=offer.validity,
)
else:
offer.date_deadline = fields.Date.add(
fields.Date.today(), days=offer.validity
)
offer.date_deadline = fields.Date.add(
offer.create_date or fields.Date.today(),
days=offer.validity,
)


def _inverse_date_deadline(self):
for offer in self:
if offer.create_date:
offer.validity = (offer.date_deadline - offer.create_date.date()).days
else:
offer.validity = (offer.date_deadline - fields.Date.today()).days
Comment on lines +48 to +52
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
for offer in self:
if offer.create_date:
offer.validity = (offer.date_deadline - offer.create_date.date()).days
else:
offer.validity = (offer.date_deadline - fields.Date.today()).days
date = offer.create_date.date() if offer.create_date else fields.Date.today()
offer.validity = (offer.date_deadline - date).days


def action_accept_offer(self):
for offer in self:
if offer.property_id.buyer_id:
raise UserError(
"a buyer is already assigned , therefore another offer has been accepted"
)
offer.state = "accepted"
offer.property_id.selling_price = self.price
offer.property_id.buyer_id = self.partner_id

return True

def action_refuse_offer(self):
for offer in self:
offer.state = "refused"
return True

@api.model
def create(self, vals_list):
for vals in vals_list:
higher_offer = self.search_count(
[
("property_id.id", "=", vals["property_id"]),
("price", ">", vals["price"]),
],
1,
)
if higher_offer:
raise UserError("Can't create an offer with a lower price")

self.env["estate.property"].browse(
vals["property_id"]
).state = "offer_received"

return super().create(vals_list)
13 changes: 13 additions & 0 deletions estate/models/estate_property_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from odoo import fields, models


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

name = fields.Char(required=True)
color = fields.Integer()
_check_name_unique = models.Constraint(
"UNIQUE(name)", "A property tag name must be unique."
)
22 changes: 22 additions & 0 deletions estate/models/estate_property_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from odoo import api, fields, models


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

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

_check_name_unique = models.Constraint(
"UNIQUE(name)", "A property type name must be unique."
)

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


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

property_ids = fields.One2many(
"estate.property",
"salesperson_id",
domain=[("state", "not in", ["sold", "cancelled"])],
)
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
estate_property_access_user,estate.property.user,model_estate_property,base.group_user,1,1,1,1
estate_property_type_access_user,estate.property.type.user,model_estate_property_type,base.group_user,1,1,1,1
estate_property_tag_access_user,estate.property.tag.user,model_estate_property_tag,base.group_user,1,1,1,1
estate_property_offer_access_user,estate.property.offer.user,model_estate_property_offer,base.group_user,1,1,1,1
1 change: 1 addition & 0 deletions estate/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import test_estate_property
42 changes: 42 additions & 0 deletions estate/tests/test_estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from odoo.exceptions import ValidationError
from odoo.tests import TransactionCase
from odoo import Command


class TestEstateProperty(TransactionCase):

@classmethod
def setUpClass(cls):
super().setUpClass()
cls.estate = cls.env['estate.property'].create({
'name': 'Super test estate',
'expected_price': 100000.0,
'state': 'new',
})
cls.test_partner = cls.env['res.partner'].create({
'name': 'Maman ours',
})

def test_estate_best_price(self):
'''
Ensure best price is correctly updated when an offer is received.
'''
self.assertEqual(self.estate.best_price, 0.0)
self.estate.offer_ids = [Command.create({
'price': 125000.0,
'partner_id': self.test_partner.id,
})]
self.assertEqual(self.estate.best_price, 125000.0)

def test_accept_offer_south_facing_garden(self):
'''
Ensure offers for estates with south-facing gardens can only be accepted if above expected
price.
'''
self.estate.expected_price = 500000
self.estate.offer_ids = [Command.create({
'price': 475000.0,
'partner_id': self.test_partner.id,
})]
with self.assertRaises(ValidationError):
self.estate.offer_ids.accept_offer()
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_menu_root" name="Estate">
<menuitem id="estate_menu_advertisement" name="Advertisement">
<menuitem id="estate_property_menu" 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>
Comment on lines +7 to +10
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

XMLid naming is not correct here

</menuitem>
</odoo>
Loading