Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test commit & push MEPL #166

Open
wants to merge 6 commits into
base: 18.0
Choose a base branch
from
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
23 changes: 23 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
'name': 'Real Estate',
'version': '1.0',
'category': 'Tutorials',
'depends': [
'base'
],
'data': [
'security/ir.model.access.csv',

'views/estate_property_offer_views.xml',
'views/estate_property_type_views.xml',
'views/estate_property_tag_views.xml',
'views/estate_property_views.xml',
'views/estate_user_views.xml',

'views/estate_menu_views.xml'
],
'installable': True,
'application': True,
'auto_install': False,
'license': 'AGPL-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 property
from . import property_offer
from . import property_tag
from . import property_type
from . import res_user
130 changes: 130 additions & 0 deletions estate/models/property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
from dateutil.relativedelta import relativedelta

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


class Property(models.Model):
_name = 'estate.property'
_description = 'Property'
_sql_constraints = [
('check_expected_price', 'CHECK(expected_price > 0)', 'The Expected Price must be positive.'),
('check_selling_price', 'CHECK(selling_price >= 0)', 'The Selling Price must be positive.'),
('check_bedrooms', 'CHECK(bedrooms >= 0)', 'The number of bedrooms must be positive.'),
('check_living_area', 'CHECK(living_area > 0)', 'The living area must be positive.'),
('check_facades', 'CHECK(facades > 0)', 'The number of facades must be positive.'),
('check_name_unique', 'UNIQUE(name)', 'The Property name must be unique.')
Copy link

Choose a reason for hiding this comment

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

This constraint was not asked by the tutorial. You can add it if you feel it is better, as it is a tutorial, but you might want to adapt the corresponding field... As of now, duplicating the property raises an error.

]
_order = 'id desc'

name = fields.Char(string='Title', required=True)
description = fields.Text()
postcode = fields.Char()
date_availability = fields.Date(string='Available From', copy=False,
default=lambda self: fields.Datetime.today() + relativedelta(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(string='Living Area (m²)')
facades = fields.Integer()
garage = fields.Boolean()
garden = fields.Boolean()
garden_area = fields.Integer(string='Garden Area (m²)')
garden_orientation = fields.Selection([
('north', 'North'),
('east', 'East'),
('south', 'South'),
('west', 'West')
])
active = fields.Boolean(default=True)
state = fields.Selection([
('new', 'New'),
('offer_received', 'Offer Received'),
('offer_accepted', 'Offer Accepted'),
('sold', 'Sold'),
('canceled', 'Canceled')
], default='new', required=True)
property_type_id = fields.Many2one('estate.property.type', string='Property Type', required=True)
buyer_id = fields.Many2one('res.partner', string='Buyer', copy=False)
salesperson_id = fields.Many2one('res.users', string='Sales Person', 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')
total_area = fields.Integer(string='Total Area (m²)', compute='_compute_total_area')
best_price = fields.Float(compute='_compute_best_price')

@api.depends('living_area', 'garden_area')
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 len(record.offer_ids) == 0:
record.best_price = 0
else:
record.best_price = max(record.offer_ids.mapped('price'))

@api.onchange('garden')
def _onchange_garden(self):
if self.garden:
self.garden_orientation = 'north'
self.garden_area = 10
else:
self.garden_orientation = None
self.garden_area = 0

@api.constrains('selling_price', 'expected_price')
def _check_date_end(self):
for record in self:
if not float_is_zero(record.selling_price, precision_digits=2) and float_compare(record.selling_price,
0.9 * record.expected_price,
precision_digits=2) < 0:
Comment on lines +81 to +83
Copy link

Choose a reason for hiding this comment

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

Suggested change
if not float_is_zero(record.selling_price, precision_digits=2) and float_compare(record.selling_price,
0.9 * record.expected_price,
precision_digits=2) < 0:
if (
not float_is_zero(record.selling_price, precision_digits=2)
and float_compare(record.selling_price, 0.9 * record.expected_price, precision_digits=2) < 0
):

This is overall preferable and more clear (nitpicky, I admit) :D

raise ValidationError("The selling price must not be below 90% of the expected price.")

def action_sold(self):
for record in self:
if record.state in ['sold', 'canceled']:
raise UserError('Cannot mark as sold.')
record.state = 'sold'
return True

def action_canceled(self):
for record in self:
if record.state in ['sold', 'canceled']:
raise UserError('Cannot mark as canceled.')
record.state = 'canceled'
return True

def action_reset(self):
for record in self:
if record.state not in ['sold', 'canceled']:
raise UserError('Cannot reset.')
record.state = 'new'
return True

# Make sure that only one offer is accepted
def set_accepted_offer(self, offer):
for record in self:
if offer is not None:
record.state = 'offer_accepted'
record.selling_price = offer.price
record.buyer_id = offer.partner_id
else:
record.state = 'offer_received'
record.selling_price = 0
record.buyer_id = None
for o in record.offer_ids:
if o.state == 'accepted' and (offer is None or o.id != offer.id):
o.action_reset(propagate=False)

def offer_created(self):
for record in self:
if record.state == 'new':
record.state = 'offer_received'

@api.ondelete(at_uninstall=False)
def _unlink_except_new_or_canceled(self):
if any((record.state not in ['canceled', 'new']) for record in self):
raise UserError("Can't delete a property with offers!")
74 changes: 74 additions & 0 deletions estate/models/property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from dateutil.relativedelta import relativedelta

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


class PropertyOffer(models.Model):
_name = 'estate.property.offer'
_description = 'Property Offer'
_sql_constraints = [
('check_offer_price', 'CHECK(price > 0)', 'The Offer Price must be positive.'),
('check_validity', 'CHECK(validity >= 0)', 'The Offer Validity must be positive.'),
]
_order = 'price desc'

state = fields.Selection([
('received', 'Received'),
('accepted', 'Accepted'),
('refused', 'Refused')
], default='received', required=True, readonly=True)
partner_id = fields.Many2one('res.partner', string='Partner', required=True)
price = fields.Float()
property_id = fields.Many2one('estate.property', string='Property', required=True)
create_date = fields.Date(string='Create Date', default=lambda self: fields.Datetime.now())
Copy link

Choose a reason for hiding this comment

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

create_date is an automatic field and should not be added here (see https://www.odoo.com/documentation/18.0/developer/tutorials/server_framework_101/03_basicmodel.html#:~:text=of%20the%20model.-,create_date,-().

I see you're already using fields.Datetime.now(): you can just use it instead of create_date if create_date is not set, in the relevant methods :)

validity = fields.Integer(string='Validity (days)', default=7)
deadline_date = fields.Date(string='Deadline Date', compute='_compute_deadline_date',
inverse='_inverse_deadline_date')
property_type_id = fields.Many2one('estate.property.type', related='property_id.property_type_id')

@api.depends('validity', 'deadline_date', 'create_date')
def _compute_deadline_date(self):
for record in self:
record.deadline_date = record.create_date + relativedelta(days=record.validity)

@api.depends('validity', 'deadline_date', 'create_date')
Copy link

Choose a reason for hiding this comment

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

This is not needed for an inverse method (I guess it was just copy-pasted from above)

def _inverse_deadline_date(self):
for record in self:
record.validity = (record.deadline_date - record.create_date).days

def action_accept(self, propagate=True):
for record in self:
if propagate:
record.property_id.set_accepted_offer(record)
record.state = 'accepted'
return True

def action_refuse(self, propagate=True):
for record in self:
if record.state == 'accepted' and propagate:
record.property_id.set_accepted_offer(None)
record.state = 'refused'
return True

def action_reset(self, propagate=True):
for record in self:
if record.state == 'accepted' and propagate:
record.property_id.set_accepted_offer(None)
record.state = 'received'
return True
Comment on lines +40 to +59
Copy link

Choose a reason for hiding this comment

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

It's better to bring all code about set_accepted_offer in this model and remove it from estate.property as the action comes from here

Copy link
Author

Choose a reason for hiding this comment

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

I was doing this because of ownership. Isn't it better to let each class manage its own variables ?

Copy link

@proose proose Oct 25, 2024

Choose a reason for hiding this comment

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

While it can be true in "classic" frameworks, in Odoo, a lot of modules depend on each other, and might want to adapt the behavior of the inherited model, or add features. If we add everything in the inherited class, it would become monstrously huge and hard to work in.

In that sense, we can apply the same thinking with files within a same module: as the "offer acceptance feature" comes from this file, it can handle the property variables by itself. We don't usually use setters and getters unless there are strong conditions to respect.

At the end of the day, it is just a (heavy) preference and your way of doing might be accepted, I was particularly nitpicky because your implem was very well done 😄


@api.model_create_multi
def create(self, vals):
res = super(models.Model, self).create(vals)
Copy link

Choose a reason for hiding this comment

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

It is usually best to put the super().create at the end of the method, especially if you might raise an error and want to prevent records creation in your method. In your implementation, the created records should be automatically deleted by the ORM when an error occurs, but you never know if the super create did not already do some changes in the DB.

You'll have to work with the list of dicts in vals (a bit more annoying to work with but not a lot)

for record in res:
if (record.property_id.state in ['canceled', 'sold']):
raise UserError('Cannot create new offers in Sold or Canceled property.')

best_offer = max(record.property_id.offer_ids.mapped('price')) if len(
record.property_id.offer_ids) > 0 else 0
if (record.price < best_offer):
raise UserError(f'The offer must be higher than {best_offer}')

record.property_id.offer_created()
Copy link

Choose a reason for hiding this comment

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

Like for set_accepted_offer: bring the code of offer_created over here :)

return res
13 changes: 13 additions & 0 deletions estate/models/property_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from odoo import models, fields


class PropertyTag(models.Model):
_name = 'estate.property.tag'
_description = 'Property Tag'
_sql_constraints = [
('check_name_unique', 'UNIQUE(name)', 'The Tag name must be unique.')
]
_order = 'name asc'

name = fields.Char(string='Tag', required=True)
color = fields.Integer(string='Color')
21 changes: 21 additions & 0 deletions estate/models/property_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from odoo import models, fields, api


class PropertyType(models.Model):
_name = 'estate.property.type'
_description = 'Property Type'
_sql_constraints = [
('check_name_unique', 'UNIQUE(name)', 'The type name must be unique.')
]
_order = 'sequence asc, name asc'

name = fields.Char(string='Title', required=True)
Copy link

Choose a reason for hiding this comment

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

The constraint prevent duplication, try to find how to allow duplication for the record while still respecting the constraint 😁

sequence = fields.Integer('Sequence', default=1)
property_ids = fields.One2many('estate.property', 'property_type_id')
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)
8 changes: 8 additions & 0 deletions estate/models/res_user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from odoo import models, fields


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

property_ids = fields.One2many('estate.property', 'salesperson_id', string='Properties',
domain="[('date_availability', '&lt;=', context_today().strftime('%Y-%m-%d'))]")
Copy link

Choose a reason for hiding this comment

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

Suggested change
domain="[('date_availability', '&lt;=', context_today().strftime('%Y-%m-%d'))]")
domain="[('date_availability', '<=', fields.Date.today())])

It doesn't work the way you did: you can try yourself by going to the Setting App -> Users & Companies and looking into the user you set as salesperson (with unavailable properties set up for him): you'll see that even unavailable properties are still shown by default

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_type,access_estate_property_type,model_estate_property_type,base.group_user,1,1,1,1
access_estate_property_tag,access_estate_property_tag,model_estate_property_tag,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_property,access_estate_property,model_estate_property,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.
12 changes: 12 additions & 0 deletions estate/views/estate_menu_views.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="Real Estate" web_icon="estate,static/description/icon.png">
<menuitem id="estate_advertisements_menu" name="Advertisements">
<menuitem id="estate_properties_menu" action="estate_property_action"/>
</menuitem>
<menuitem id="estate_settings_menu" name="Settings">
<menuitem id="estate_property_type_menu" action="estate_property_type_action"/>
<menuitem id="estate_property_tag_menu" action="estate_property_tag_action"/>
</menuitem>
</menuitem>
</odoo>
31 changes: 31 additions & 0 deletions estate/views/estate_property_offer_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<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">
<field name="price" optional="show"/>
<field name="partner_id"/>
<field name="deadline_date" optional="show"/>
<field name="validity" optional="hide"/>
<button name="action_accept" string="Accept" invisible="state in ['accepted', 'refused']" type="object"
icon="fa-check"/>
<button name="action_refuse" string="Refuse" invisible="state in ['accepted', 'refused']" type="object"
icon="fa-times"/>
<field name="state" widget="badge"
decoration-success="state == 'accepted'"
decoration-info="state == 'received'"
decoration-danger="state == 'refused'"
optional="show"/>
</list>
</field>
</record>

<record id="estate_property_offer_action" model="ir.actions.act_window">
<field name="name">Property Types</field>
Copy link

Choose a reason for hiding this comment

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

Forgot to change the name here ? :D

<field name="res_model">estate.property.offer</field>
<field name="domain">[('property_type_id', '=', active_id)]</field>
<field name="view_mode">list</field>
</record>
</odoo>
28 changes: 28 additions & 0 deletions estate/views/estate_property_tag_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="estate_property_tag_view_list" model="ir.ui.view">
<field name="name">estate.property.tag.list</field>
<field name="model">estate.property.tag</field>
<field name="arch" type="xml">
<list string="Property tags" editable="bottom">
<field name="name"/>
</list>
</field>
</record>

<record id="estate_property_tag_view_filter" model="ir.ui.view">
<field name="name">estate.property.tag.list.select</field>
<field name="model">estate.property.tag</field>
<field name="arch" type="xml">
<search string="Search Property tag">
<field name="name"/>
</search>
</field>
</record>

<record id="estate_property_tag_action" model="ir.actions.act_window">
<field name="name">Property tags</field>
<field name="res_model">estate.property.tag</field>
<field name="view_mode">list</field>
</record>
</odoo>
Loading