Skip to content

[ADD] estate: add an estate module #790

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 19 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
19 commits
Select commit Hold shift + click to select a range
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': 'E-State!',
'summary': 'Manage real estate advertisements',
'description': """
Manage advertisements and offers for different real estate properties as part of the "Sever framework 101" tutorial
""",
'author': 'Odoo',
'website': 'https://www.odoo.com',
'version': '0.1',
'application': True,
'installable': True,
'depends': ['base'],
'data': [
'views/estate_property_views.xml',
'views/estate_property_offer_views.xml',
'views/estate_property_tag_views.xml',
'views/estate_property_type_views.xml',
'views/estate_menus.xml',
'views/res_users.xml',
'security/ir.model.access.csv',
],
'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 estate_property
from . import estate_property_offer
from . import estate_property_tag
from . import estate_property_type
from . import res_users
113 changes: 113 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
from odoo import api, fields, models
from odoo.exceptions import UserError, ValidationError
from odoo.tools.date_utils import relativedelta
from odoo.tools.float_utils import float_compare, float_is_zero


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

name = fields.Char('Name', required=True)
property_type_id = fields.Many2one('estate.property.type', string='Property Type')
description = fields.Text('Description')
postcode = fields.Char('Postcode')
date_availability = fields.Date('Available From', default=fields.Date.today() + relativedelta(months=3), copy=False)

Choose a reason for hiding this comment

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

Suggested change
date_availability = fields.Date('Available From', default=fields.Date.today() + relativedelta(months=3), copy=False)
date_availability = fields.Date(
'Available From',
default=lambda self: fields.Date.today() + relativedelta(months=3),
copy=False
)

As this value should be recomputed, prefer using a lambda function. The current way stores the value at the server start.

expected_price = fields.Float('Expected Price', required=True)
selling_price = fields.Float('Selling Price', readonly=True, copy=False)
best_price = fields.Float('Best Offer', compute='_compute_best_price')
bedrooms = fields.Integer('Bedrooms', default=2)
living_area = fields.Integer('Living Area (sqm)')
facades = fields.Integer('Facades')
garage = fields.Boolean('Has Garage')
garden = fields.Boolean('Has Garden')
garden_area = fields.Integer('Garden Area (sqm)')
garden_orientation = fields.Selection(string='Garden Orientation', selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')])
total_area = fields.Float('Total Area (sqm)', compute='_compute_total_area')
salesperson_id = fields.Many2one('res.users', string='Salesperson', default=lambda self: self.env.user)
buyer_id = fields.Many2one('res.partner', string='Buyer', copy=False)
offer_ids = fields.One2many('estate.property.offer', 'property_id', string='Offers')
tag_ids = fields.Many2many('estate.property.tag', string='Tags')
state = fields.Selection(
string='State',
selection=[
('new', 'New'),
('offer_received', 'Offer Received'),
('offer_accepted', 'Offer Accepted'),
('sold', 'Sold'),
('cancelled', 'Cancelled'),
],
required=True,
copy=False,
default='new',
)
active = fields.Boolean('Active', default=True)

_sql_constraints = [
('check_expected_price', 'CHECK(expected_price > 0)', 'The expected price should be strictly positive.'),
('check_selling_price', 'CHECK(selling_price >= 0)', 'The selling price should be positive.'),
('check_garden_area', 'CHECK(garden = false OR garden_area > 0)', 'The garden area should be strictly positive.'),
('check_garden_orientation', "CHECK(garden = false OR garden_orientation IN ('north', 'south', 'east', 'west'))", 'You should choose a garden orientation.'),
]

@api.ondelete(at_uninstall=False)
def _unlink_with_check_state(self):
for record in self:
if record.state not in ['new', 'cancelled']:
raise UserError(self.env._("You cannot delete a property unless it is 'New' or 'Cancelled'."))

@api.constrains('selling_price')
def _check_selling_price(self):
for record in self:
if not float_is_zero(record.selling_price, 2):
if record.expected_price:
if float_compare(record.selling_price, record.expected_price * 0.9, 2) < 0:
raise ValidationError(self.env._('The selling price must be at least 90% of the expected price.'))

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

@api.depends('offer_ids')
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 = None

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

def action_set_sold(self):
self.ensure_one()

if self.state == 'cancelled':
raise UserError(self.env._('Cancelled properties cannot be sold.'))

self.state = 'sold'

def action_set_cancelled(self):
self.ensure_one()

if self.state == 'sold':
raise UserError(self.env._('Sold properties cannot be cancelled.'))

self.state = 'cancelled'
68 changes: 68 additions & 0 deletions estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from odoo import api, fields, models
from odoo.exceptions import UserError
from odoo.tools.date_utils import relativedelta


class EstatePropertyOffer(models.Model):
_name = 'estate.property.offer'
_description = 'Real Estate Property Offer'
_order = 'price desc'

price = fields.Float()
partner_id = fields.Many2one('res.partner', string='Partner', required=True)
property_id = fields.Many2one('estate.property', string='Property', required=True)
date_deadline = fields.Date('Deadline')
validity = fields.Integer('Validity (days)', default=7, compute='_compute_validity', inverse='_inverse_validity')
status = fields.Selection(
string='Status',
selection=[
('accepted', 'Accepted'),
('refused', 'Refused'),
],
copy=False,
)
property_type_id = fields.Many2one(related='property_id.property_type_id')
property_state = fields.Selection(related='property_id.state')
active = fields.Boolean('Active', default=True)

_sql_constraints = [('check_price', 'CHECK(price > 0)', 'The price must be strictly positive.')]

@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
p = self.env['estate.property'].browse(vals['property_id'])
if p.best_price > vals['price']:
raise UserError(self.env._('An offer with a higher price already exists.'))
if p.state == 'new':
p.state = 'offer_received'
return super().create(vals_list)

@api.depends('date_deadline')
def _compute_validity(self):
for record in self:
if record.date_deadline:
record.validity = (record.date_deadline - fields.date.today()).days

def _inverse_validity(self):
for record in self:
if isinstance(record.validity, int):
record.date_deadline = fields.date.today() + relativedelta(days=record.validity)

def action_accept(self):
if self.property_id.state in ['offer_accepted', 'sold'] and self.status != 'accepted':
raise UserError(self.env._('Another offer has already been accepted.'))
self.property_id.buyer_id = self.partner_id
self.property_id.selling_price = self.price
self.property_id.state = 'offer_accepted'
for offer in self.property_id.offer_ids:
offer.status = 'refused'
self.status = 'accepted'

def action_refuse(self):
if self.status == 'accepted':
if self.property_id.state == 'sold':
raise UserError(self.env._('The property has already been sold.'))
self.property_id.buyer_id = None
self.property_id.selling_price = None
self.property_id.state = 'offer_received'
self.status = 'refused'
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 = 'Real Estate Property Tag'
_order = 'name'

name = fields.Char('Name', required=True)
color = fields.Integer('Color')
active = fields.Boolean('Active', default=True)

_sql_constraints = [('unique_tag_name', 'UNIQUE(name)', 'Tag name should 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 api, fields, models


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

name = fields.Char('Name', required=True)
property_ids = fields.One2many('estate.property', 'property_type_id', string='Properties')
offer_ids = fields.One2many('estate.property.offer', 'property_type_id', string='Offers')
offer_count = fields.Integer('Offer Count', compute='_compute_offer_count')
sequence = fields.Integer('Sequence', help='Used to order property types. Lower is better.')
active = fields.Boolean('Active', default=True)

_sql_constraints = [('unique_type_name', 'UNIQUE(name)', 'Type name should 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/res_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from odoo import fields, models


class User(models.Model):
_inherit = ['res.users']

property_ids = fields.One2many('estate.property', 'salesperson_id', string='Properties', 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,access_estate_property,model_estate_property,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_tag,access_estate_property_tag,model_estate_property_tag,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
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="Real Estate">
<menuitem id="estate_advertisements_menu" name="Advertisements">
<menuitem id="estate_property_menu_action" 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>
</menuitem>
</odoo>
38 changes: 38 additions & 0 deletions estate/views/estate_property_offer_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?xml version="1.0"?>
<odoo>
<record id="estate_property_offer_list_view" 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"
decoration-success="status == 'accepted'" decoration-danger="status == 'refused'">
<field name="price" />
<field name="partner_id" />
<field name="validity" />
<field name="date_deadline" />
<button name="action_accept" string="Accept" type="object" icon="fa-check"
invisible="property_state in ['sold', 'cancelled', 'offer_accepted'] or status == 'accepted'" />
<button name="action_refuse" string="Refuse" type="object" icon="fa-times"
invisible="property_state in ['sold', 'cancelled'] or status == 'refused'" />
</list>
</field>
</record>

<record id="estate_property_offer_form_view" 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="Estate Property Offer">
<sheet>
<group>
<field name="price" />
<field name="partner_id" />
<field name="validity" />
<field name="date_deadline" />
<field name="status" />
</group>
</sheet>
</form>
</field>
</record>
</odoo>
18 changes: 18 additions & 0 deletions estate/views/estate_property_tag_views.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0"?>
<odoo>
<record id="estate_property_tag_list_view" 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_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