Skip to content

[ADD] estate: it creates a basic estate application #787

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

Open
wants to merge 20 commits into
base: 18.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a67237e
[ADD] Create a basic estate application
MaximeNoirhomme229 May 19, 2025
dddec52
[ADD] add menus (chap 5)
MaximeNoirhomme229 May 20, 2025
49cc7ee
[FIX] estate: resolve time deltas based on timezones
barracudapps May 20, 2025
d5eecbf
[FIX] add license and date availability 3 mouth instead of 1
MaximeNoirhomme229 May 20, 2025
1a1ac3a
[ADD] Add form and list view with filter on status and group by on p…
MaximeNoirhomme229 May 20, 2025
430fb4b
[ADD] add link with user and partner model + create estate property type
MaximeNoirhomme229 May 20, 2025
4bb21bc
[FIX] style
MaximeNoirhomme229 May 21, 2025
9d7fc0b
[FIX] give a proper name to the estate property type class and rename…
MaximeNoirhomme229 May 21, 2025
ff9a522
[ADD] estate: add tags and offer to the estate view
MaximeNoirhomme229 May 21, 2025
eedcbf4
[ADD] estate: compute best offer, total area, offer deadline (chap 8)
MaximeNoirhomme229 May 21, 2025
c51ae78
[ADD] estate: it can cancel/sold a property and accept or refuse an o…
MaximeNoirhomme229 May 21, 2025
d36ea31
[ADD] estate: it creates sql and python contraints
MaximeNoirhomme229 May 21, 2025
1edc71a
[IMP] estate: it reorganises the imports to make them clearer
MaximeNoirhomme229 May 21, 2025
2a74e2f
[IMP] estate: it simplifies available filter using in keyword
MaximeNoirhomme229 May 21, 2025
c4c7c03
[ADD] estate: it adds sprinkle to the interface (chap 11)
MaximeNoirhomme229 May 23, 2025
759c68e
[FIX] estate: it fixes style
MaximeNoirhomme229 May 23, 2025
5ede1fe
[FIX] estate: it remove the local config from .gitignore file
MaximeNoirhomme229 May 23, 2025
e844769
[FIX] estate: it removes white space
MaximeNoirhomme229 May 23, 2025
63033b3
[FIX] estate: it removes white space
MaximeNoirhomme229 May 23, 2025
6ad2139
[ADD] estate: chap 12 to chap 14
MaximeNoirhomme229 May 26, 2025
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
15 changes: 15 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
'name': 'A real state module',
'version': '1.0',
'depends': ['base'],
'application': True,
'license': 'LGPL-3',
'data': [
'security/ir.model.access.csv',
'views/view_properties.xml',
'views/view_properties_type.xml',
'views/view_properties_tag.xml',
'views/estate_menus.xml',
'views/view_properties_user.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_offer
from . import estate_property_tag
from . import estate_property_type
from . import estate_user
104 changes: 104 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
from dateutil.relativedelta import relativedelta
from odoo import api, exceptions, fields, models
from odoo.tools.float_utils import float_compare


class EstateProperty(models.Model):
_name = 'estate.property'
_description = 'estate property module'
Comment on lines +7 to +8

Choose a reason for hiding this comment

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

Suggested change
_name = 'estate.property'
_description = 'estate property module'
_name = "estate.property"
_description = "estate property module"

Keep double quotes for private properties

_order = 'id desc'

name = fields.Char(required=True)
description = fields.Text("Description")
date_availability = fields.Date(
"Date Availability", default=fields.Date.today() + relativedelta(months=3), copy=False
)
expected_price = fields.Float("Expected Price", required=True)
selling_price = fields.Float("Selling Price", default=0, readonly=True, copy=False)
bedrooms = fields.Integer(required=True, default=2)
living_area = fields.Integer("Living Area (sqm)", required=True)
facades = fields.Integer(default=0)
garage = fields.Boolean(default=False)
garden = fields.Boolean(default=False)
garden_area = fields.Integer("Garden Area (sqm)")
garden_orientation = fields.Selection([('north', 'North'), ('east', 'East'), ('south', 'South'), ('west', 'West')])
total_area = fields.Integer("Total Area (sqm)", compute='_compute_total_area')
postcode = fields.Integer()
active = fields.Boolean(default=True)
status = fields.Selection(
[
('new', 'New'),
('offer received', 'Offer Received'),
('offer accepted', 'Offer Accepted'),
('sold', 'Sold'),
('cancelled', 'Cancelled'),
],
default='new',
copy=False,
)
estate_type_id = fields.Many2one(comodel_name='estate.property.type')
estate_tag_ids = fields.Many2many(comodel_name='estate.property.tag')
estate_offer_ids = fields.One2many('estate.property.offer', 'property_id', string='Offers')
partner_id = fields.Many2one(comodel_name='res.partner')
user_id = fields.Many2one(comodel_name='res.users', default=lambda self: self.env.user)
best_offer = fields.Float(compute='_compute_best_price')
_sql_constraints = [
(
'check_stricly_positive_expected_price',
'CHECK(expected_price > 0)',
'The expected price of an estate property must be strictly positive.',
),
(
'check_positive_selling_price',
'CHECK(selling_price >= 0)',
'The selling price of an estate property must be positive.',
),
]

@api.constrains('selling_price', 'expected_price')
def _check_valid_selling_price(self):
for record in self:
if not (any(offer.status == 'accepted' for offer in self.estate_offer_ids)):
continue

if float_compare(record.selling_price, 0.9 * record.expected_price, precision_digits=2) < 0:
raise exceptions.ValidationError("The selling price must not be below 90 % of the expected price")

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

Choose a reason for hiding this comment

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

What happens here if there is no garden ?


@api.depends('estate_offer_ids.price')
def _compute_best_price(self):
for record in self:
if len(record.estate_offer_ids) == 0:
record.best_offer = 0
else:
record.best_offer = max(record.estate_offer_ids.mapped('price'))

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

def action_cancel(self):
if self.status == 'sold':
raise exceptions.UserError('A sold property cannot be cancelled')

self.status = 'cancelled'

def action_sold(self):
if self.status == 'cancelled':
raise exceptions.UserError('A cancelled property cannot be sold')

self.status = 'sold'

@api.ondelete(at_uninstall=False)
def _unlink_if_estate_is_not_new_nor_cancelled(self):
if any(estate.status!='new' and estate.status!='cancelled' for estate in self):
raise exceptions.UserError('Can only delete new and cancelled records!')
93 changes: 93 additions & 0 deletions estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
from dateutil.relativedelta import relativedelta
from odoo import api, exceptions, fields, models


class EstatePropertyOffer(models.Model):
_name = 'estate.property.offer'
_description = 'estate property offer module'
_order = 'price desc'

name = fields.Char(required=True)
price = fields.Float()
status = fields.Selection(
[
('accepted', 'Accepted'),
('refused', 'Refused'),
],
copy=False,
)

validity = fields.Integer(default=7)
create_date = fields.Date(default=fields.Date.today(), readonly=True)
date_deadline = fields.Date(compute='_compute_deadline', inverse='_compute_inverse_deadline')

partner_id = fields.Many2one(comodel_name='res.partner', required=True)
property_id = fields.Many2one(comodel_name='estate.property', required=True)
property_type_id = fields.Many2one(comodel_name='estate.property.type', related='property_id.estate_type_id')

_sql_constraints = [
(
'check_stricly_positive_price',
'CHECK(price > 0)',
'The price of an estate property offer must be strictly positive.',
),
]

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

@api.model_create_multi
def create(self, val_list):
for val in val_list:
val_property_id = val.get('property_id')
val_curr_price = val.get('price')
property_id = self.env['estate.property'].browse(val_property_id)
if not property_id:
raise exceptions.ValidationError(
f'Property with id {property_id} does not exist.Please contact the support'
)

if property_id.best_offer and property_id.best_offer > val_curr_price:
raise exceptions.UserError('The price of a new offer must be bigger than the best_price and'
f' {property_id.best_offer} is bigger than {val_curr_price}')

if property_id.status == 'new':
property_id.status = 'offer received'

return super(EstatePropertyOffer, self).create(val_list)

@api.ondelete(at_uninstall=False)
def _unlink_and_reset_state_if_no_offer_left(self):
for offer in self:
property_id = offer.property_id
if len(property_id.estate_offer_ids) == 1 and property_id.status != 'cancelled':
property_id.status = 'new'

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

def action_accepted(self):
if self.status == 'accepted':
return

if any(offer.status == 'accepted' for offer in self.property_id.estate_offer_ids):
raise exceptions.UserError('You can only accept one offer')

self.status = 'accepted'
self.property_id.selling_price = self.price
self.property_id.partner_id = self.partner_id
if self.status != 'sold':
self.property_id.status = 'offer accepted'

def action_refused(self):
if self.status == 'refuse':
return

if self.status == 'accepted':
self.property_id.selling_price = 0
self.property_id.partner_id = None

self.status = 'refused'
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 EstatePropertyTag(models.Model):
_name = 'estate.property.tag'
_description = 'estate property tag module'
_order = 'name'

name = fields.Char(required=True)
color = fields.Integer()

_sql_constraints = [
('unique_tag_name', 'UNIQUE(name)',
'The tag name must be unique'),
]
23 changes: 23 additions & 0 deletions estate/models/estate_property_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from odoo import api, fields, models


class EstatePropertyType(models.Model):
_name = 'estate.property.type'
_description = 'estate property type module'
_order = 'name'

name = fields.Char(required=True)
sequence = fields.Integer('Sequence', default=1, help="Used to order types. Lower is better.")
property_ids = fields.One2many('estate.property', 'estate_type_id')
offer_ids = fields.One2many('estate.property.offer', 'property_type_id')
offer_count = fields.Integer(compute='_compute_offer_count')

_sql_constraints = [
('unique_property_type', 'UNIQUE(name)',
'The property type must be unique'),
]

@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/estate_user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from odoo import fields, models

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

property_ids = fields.One2many('estate.property', 'user_id', domain=[('status', '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
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
estate.access_estate_property_tag,access_estate_property_tag,estate.model_estate_property_tag,base.group_user,1,1,1,1
estate.access_estate_property_offer,access_estate_property_offer,estate.model_estate_property_offer,base.group_user,1,1,1,1
11 changes: 11 additions & 0 deletions estate/views/estate_menus.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<odoo>
<menuitem id="estate_menu_root" name="Estate">
<menuitem id="estate_first_level_menu" name="Advertissements">
<menuitem id="estate_model_menu_action" action="estate_model_action"/>
</menuitem>
<menuitem id="estate_settings_menu" name="Settings">
<menuitem id="estate_type_model_menu_action" action="estate_property_type_model_action"/>
<menuitem id="estate_tag_model_menu_action" action="estate_property_tag_model_action"/>
</menuitem>
</menuitem>
</odoo>
Loading