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
22 changes: 22 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
'name': 'estate',
'depends': [
'base'
],
'installable': True,
Comment thread
nausicaa73 marked this conversation as resolved.
'application': True,
'author': 'vibad',
'license': 'LGPL-3',
'version': '1.0',
'data': [
'security/ir.model.access.csv',
'view/estate_property_offer_views.xml',
'view/estate_property_views.xml',
'view/estate_property_type_views.xml',
'view/estate_property_tag_views.xml',
'view/estate_inherit_view.xml',
'view/estate_action.xml',
],


}
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 inherited_model
115 changes: 115 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
from odoo import api, fields, models
from odoo.exceptions import UserError, ValidationError


class Estate_property(models.Model):
Comment thread
nausicaa73 marked this conversation as resolved.
_name = "estate.property"
_description = "APP super mega trop bien"
_order = "id desc"

name = fields.Char(required=True)
Comment thread
nausicaa73 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))
expected_price = fields.Float(required=True)
selling_price = fields.Float(readonly=True, copy=False)
bedrooms = fields.Integer(default=2)
living_area = fields.Integer()
facades = fields.Integer()
garage = fields.Boolean()
garden = fields.Boolean()
garden_area = fields.Integer()
garden_orientation = fields.Selection(
string="Orientation",
selection=[("north", "North"), ("south", "South"), ("east", "East"), ("west", "West")],
help="The garden orientation",
)
active = fields.Boolean(default=True)
state = fields.Selection(
string="Status",
selection=[("new", "New"), ("offer_received", "Offer Received"), ("offer_accepted", "Offer Accepted"), ("sold", "Sold"), ("cancelled", "Cancelled")],
required=True,
copy=False,
default="new",
compute="_compute_state",
store=True,
)
Comment thread
nausicaa73 marked this conversation as resolved.
property_type_id = fields.Many2one("estate.property.type", string="Property Type")
salesperson_id = fields.Many2one("res.users", string="Salesperson", default=lambda self: self.env.user)
buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False)
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_area")
best_price = fields.Float(compute="_compute_best_price")

_check_expected_price = models.Constraint(
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Order of the elements of the file is wrong, constraints should come after computes (see doc)

"CHECK(expected_price > 0)",
message="The expected price must be strictly positive",
)

_check_selling_price = models.Constraint(
"CHECK(selling_price >= 0)",
message="The selling price cannot be negative",
)

@api.depends("living_area", "garden_area")
def _compute_area(self):
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No need to shorten the name, especially since there are multiple variables named x_area in the model

Suggested change
def _compute_area(self):
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):
for record in self:
if record.offer_ids:
record.best_price = max(record.offer_ids.mapped("price"))
else:
record.best_price = 0

@api.onchange("garden")
def _onchange_garden(self):
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Tricky (?) question time!

At first glance, this method could also be an inverse. Can you tell why it makes more sense for it to be an onchange?

Answer

inverse are triggered whenever the field is written. onchange are called by form views.

It doesn't make much sense to set arbitrary default values when programmatically updating records, so onchange is the correct approach here.

for record in self:
if record.garden:
record.garden_area = 10
record.garden_orientation = "north"
else:
record.garden_area = 0
record.garden_orientation = False

@api.depends("offer_ids", "offer_ids.state")
def _compute_state(self):
for record in self:
if record.state in ["sold", "cancelled"]:
return
if record.offer_ids:
for offer in record.offer_ids:
if offer.state == "accepted":
record.state = "offer_accepted"
break
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

We want to stop after updating, so return has the same effect but is clearer as to what the intent is

Suggested change
break
return

else:
record.state = "new"

def action_sold(self):
for record in self:
if record.state != "cancelled" and record.state != "sold":
record.state = "sold"
else:
raise UserError("A property that is cancelled or already sold cannot be sold.")

def action_cancel(self):
for record in self:
if record.state != "sold" and record.state != "cancelled":
record.state = "cancelled"
else:
raise UserError("A property that is sold or already cancelled cannot be cancelled.")

@api.constrains("selling_price", "expected_price")
def _check_enough_selling_price(self):
for record in self:
if record.selling_price and record.selling_price < record.expected_price * 0.9:
raise ValidationError("The selling price cannot be less than 90% of the expected price.")

@api.ondelete(at_uninstall=False)
def _ondelete_cancel_new(self):
for record in self:
if record.state not in ["new", "cancelled"]:
raise UserError("You can only delete offers that are new or cancelled.")
78 changes: 78 additions & 0 deletions estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from odoo import api, fields, models
from odoo.exceptions import UserError


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

price = fields.Float(required=True)
partner_id = fields.Many2one("res.partner", string="Partner", required=True)
property_id = fields.Many2one("estate.property", string="Property", required=True)
state = fields.Selection([
("new", "New"),
("accepted", "Accepted"),
("refused", "Refused"),
], default="new", string="State", copy=False)
validaty = fields.Integer(string="Offer Validity (days)", default=7)
date_deadline = fields.Date(string="Offer Deadline", compute="_compute_date_deadline", inverse="_inverse_date_deadline")
property_type_id = fields.Many2one(related="property_id.property_type_id", store=True)

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

@api.depends("validaty", "create_date")
def _compute_date_deadline(self):
for record in self:
date = record.create_date
if not date:
date = fields.Date.today()
if record.validaty:
record.date_deadline = fields.Date.add(date, days=record.validaty)
else:
record.date_deadline = False
Comment on lines +28 to +36
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

To get the actual date, you need to call .date() so the line becomes date = record.create_date.date(). However you don't want to do so if record.create_date is false. Instead you can use Python's ternary (aka conditional) operations.

Also, you provide a default value for validity so you can assume a value.

Suggested change
def _compute_date_deadline(self):
for record in self:
date = record.create_date
if not date:
date = fields.Date.today()
if record.validaty:
record.date_deadline = fields.Date.add(date, days=record.validaty)
else:
record.date_deadline = False
def _compute_date_deadline(self):
for record in self:
date = record.create_date.date() if record.create_date else fields.Date.today()
record.date_deadline = fields.Date.add(date, days=record.validaty)


def _inverse_date_deadline(self):
for record in self:
if record.date_deadline and record.create_date:
create_date = fields.Date.to_date(record.create_date)
record.validaty = (record.date_deadline - create_date).days
else:
record.validaty = 0
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Shouldn't you default to 7?


def accept_offer(self):
for record in self:
if record.state == "accepted" or record.state == "refused":
raise UserError("This offer has already been accepted or refused.")
for offer in record.property_id.offer_ids:
if offer.state == "accepted":
Comment on lines +50 to +51
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is fine, but here's a one liner for comparison

Suggested change
for offer in record.property_id.offer_ids:
if offer.state == "accepted":
if "accepted" in record.mapped("property_id.offer_ids.state"):

raise UserError("Another offer has already been accepted for this property.")
if record.property_id.garden_orientation == "south" and record.price <= record.property_id.expected_price:
raise UserError("The price must be more than the expected price for properties with a south-facing garden.")
record.state = "accepted"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

When an action modifies values on a record, it usually explicitly calls a write. This way, it can easily be expanded upon in later versions if necessary.

Suggested change
record.state = "accepted"
record.write({
"state": "accepted",
})

record.property_id.selling_price = record.price
record.property_id.buyer_id = record.partner_id
return True
Comment on lines +56 to +58
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

You can

  • return the call to write,
  • directly set the property's state in the same write call to avoid another write being trigger by the compute method.
Suggested change
record.property_id.selling_price = record.price
record.property_id.buyer_id = record.partner_id
return True
return record.property_id.write({
"state": "offer_accepted",
"selling_price": record.price,
"buyer_id": record.partner_id,
})


def refuse_offer(self):
for record in self:
if record.state != "new":
raise UserError("This offer has already been accepted or refused.")
record.state = "refused"
return True
Comment on lines +64 to +65
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 remark


@api.model_create_multi
def create(self, vals_list):
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 remark for order of methods

max_price_list = 0
for vals in vals_list:
if self.env["estate.property"].browse(vals["property_id"]).state == "new":
self.env["estate.property"].browse(vals["property_id"]).state = "offer_received"
max_price = max(self.env["estate.property.offer"].search([("property_id", "=", vals["property_id"])]).mapped("price") or [0])
max_price_list = max(max_price_list, max_price)
if vals["price"] <= max_price:
raise UserError("The price must be higher than the current highest offer.") # Error for one offer blocks all offers in the list
max_price_list = max(max_price_list, vals["price"])
return super().create(vals_list)
15 changes: 15 additions & 0 deletions estate/models/estate_property_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from odoo import fields, models


class Estate_property_tag(models.Model):
_name = "estate.property.tag"
_description = "tag super mega trop bien"
_order = "name"

name = fields.Char(required=True)
color = fields.Integer()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

American english is ugly, too bad it's the default most of the time 😢

Suggested change
color = fields.Integer()
colour = fields.Integer()

(don't apply this, it's just me ranting)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

🇺🇸🇺🇸🇺🇸


_check_name = models.Constraint(
"UNIQUE(name)",
message="The name of the tag must be unique",
)
21 changes: 21 additions & 0 deletions estate/models/estate_property_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from odoo import fields, models


class Estate_property_type(models.Model):
_name = "estate.property.type"
_description = "APP super mega trop bien"
_order = "sequence, name"
_check_name = models.Constraint(
Comment thread
nausicaa73 marked this conversation as resolved.
"UNIQUE(name)",
message="The name of the property type must be unique",
)

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

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 fields, models


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

property_ids = fields.One2many("estate.property", "salesperson_id", string=" Estate 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
acces_estate_property,estate_property,model_estate_property,base.group_user,1,1,1,1
acces_estate_property_type,estate_property_type,model_estate_property_type,base.group_user,1,1,1,0
acces_estate_property_tag,estate_property_tag,model_estate_property_tag,base.group_user,1,1,1,0
acces_estate_property_offer,estate_property_offer,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/view/estate_action.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0"?>
<odoo>
<menuitem id="estate_root" name="Real Estate">
<menuitem id="advertisements_level_menu" name="Advertisements">
<menuitem id="estate_property_menu_action" action="estate_property_action"/>
Comment thread
nausicaa73 marked this conversation as resolved.
</menuitem>
<menuitem id="property_types_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>
15 changes: 15 additions & 0 deletions estate/view/estate_inherit_view.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0"?>
<odoo>
<record id="estate_inherit_view" model="ir.ui.view">
<field name="name">estate.inherit.view</field>
<field name="model">res.users</field>
<field name="inherit_id" ref="base.view_users_form"/>
<field name="arch" type="xml">
<xpath expr="//page[@name='preferences']" position="after">
<page string="Real Estate" name="real_estate">
<field name="property_ids" options="{'no_create': True, 'no_open': True}"/>
</page>
</xpath>
</field>
</record>
</odoo>
40 changes: 40 additions & 0 deletions estate/view/estate_property_offer_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?xml version="1.0"?>
<odoo>
<record id="estate_property_offer_action" model="ir.actions.act_window">
<field name="name">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>

<record id="estate_property_offer_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="Offer">
<sheet>
<group>
<field name="price" string="Price"/>
<field name="state" string="Status"/>
<field name="partner_id" string="Partner"/>
<field name="validaty" string="Validity (days)"/>
<field name="date_deadline" string="Offer Deadline"/>
</group>
</sheet>
</form>
</field>
</record>

<record id="estate_property_offer_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="Offers" editable="bottom" decoration-success="state == 'accepted'" decoration-danger="state == 'refused'">
<field name="price" string="Price"/>
<button name="accept_offer" type="object" string="Accept" icon="fa-check" invisible="state == 'accepted' or state == 'refused'"/>
<button name="refuse_offer" type="object" string="Refuse" icon="fa-close" invisible="state == 'accepted' or state == 'refused'"/>
<field name="partner_id" string="Partner"/>
</list>
</field>
</record>
</odoo>
Loading