Skip to content

Commit 7ce73f4

Browse files
committed
[ADD] real_estate: onboarding up to chapter 9
Progressed with the onboarding tutorial up to chapter 9, covering model definitions, views, relations, compute and onchange methods. Also implemented business actions for properties and offers: sold, cancel, accept, and refuse.
1 parent fbf9ee9 commit 7ce73f4

14 files changed

+420
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# Style
2+
pyproject.toml
3+
14
# Byte-compiled / optimized / DLL files
25
__pycache__/
36
*.py[cod]

estate/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import models

estate/__manifest__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
'name': 'Real Estate',
3+
'version': '1.0',
4+
'description': 'Real Estate Management System',
5+
'summary': 'Real Estate Management System',
6+
'license': 'LGPL-3',
7+
'depends': ['base'],
8+
'data': [
9+
'security/ir.model.access.csv',
10+
'views/estate_property_tag.xml',
11+
'views/estate_property_type.xml',
12+
'views/estate_property_offer.xml',
13+
'views/estate_property.xml',
14+
'views/estate_menus.xml',
15+
],
16+
'auto_install': False,
17+
'application': True,
18+
}

estate/models/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from . import (
2+
estate_property,
3+
estate_property_offer,
4+
estate_property_tag,
5+
estate_property_type,
6+
)

estate/models/estate_property.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
from dateutil.relativedelta import relativedelta
2+
from odoo import api, fields, models
3+
from odoo.exceptions import UserError
4+
5+
6+
class EstateProperty(models.Model):
7+
_name = 'estate.property'
8+
_description = 'Real estate property with offers, pricing, and availability.'
9+
10+
name = fields.Char(string='Title', required=True)
11+
description = fields.Text(string='Description')
12+
postcode = fields.Char(string='Postcode')
13+
date_availability = fields.Date(
14+
string='Availability From', copy=False, default=lambda self: fields.Date.today() + relativedelta(months=3)
15+
)
16+
expected_price = fields.Float(string='Expected Price', required=True)
17+
selling_price = fields.Float(string='Selling Price', readonly=True, copy=False)
18+
best_price = fields.Float(string='Best Price', compute='_compute_best_price', store=True)
19+
property_type_id = fields.Many2one('estate.property.type', string='Property Type')
20+
tag_ids = fields.Many2many('estate.property.tag', string='Tags')
21+
buyer_id = fields.Many2one('res.partner', string='Buyer')
22+
seller_id = fields.Many2one('res.users', string='Seller', default=lambda self: self.env.user)
23+
bedrooms = fields.Integer(string='Bedrooms', default=2)
24+
living_area = fields.Integer(string='Living Area (sqm)')
25+
facades = fields.Integer(string='Facades')
26+
garage = fields.Boolean(string='Garage')
27+
garden = fields.Boolean(string='Garden')
28+
garden_area = fields.Integer(string='Garden Area')
29+
garden_orientation = fields.Selection(
30+
[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')],
31+
string='Garden Orientation',
32+
)
33+
total_area = fields.Integer(string='Total Area (sqm)', compute='_compute_total_area', store=True)
34+
state = fields.Selection(
35+
[
36+
('new', 'New'),
37+
('offer_received', 'Offer Received'),
38+
('offer_accepted', 'Offer Accepted'),
39+
('sold', 'Sold'),
40+
('cancelled', 'Cancelled'),
41+
],
42+
string='State',
43+
default='new',
44+
required=True,
45+
copy=False,
46+
compute='_compute_state',
47+
store=True,
48+
)
49+
offer_ids = fields.One2many('estate.property.offer', 'property_id', string='Offers')
50+
active = fields.Boolean(string='Active', default=True)
51+
52+
###### COMPUTE ######
53+
@api.depends('living_area', 'garden_area')
54+
def _compute_total_area(self):
55+
for record in self:
56+
record.total_area = record.living_area + record.garden_area
57+
58+
@api.depends('offer_ids.price')
59+
def _compute_best_price(self):
60+
for record in self:
61+
prices = record.offer_ids.mapped('price')
62+
if prices:
63+
record.best_price = max(prices)
64+
else:
65+
record.best_price = 0.0
66+
67+
@api.depends('offer_ids')
68+
def _compute_state(self):
69+
for record in self:
70+
if not record.offer_ids:
71+
record.state = 'new'
72+
elif record.state == 'new':
73+
record.state = 'offer_received'
74+
75+
###### ONCHANGE ######
76+
@api.onchange('garden')
77+
def _onchange_garden(self):
78+
if self.garden:
79+
self.garden_area = 10
80+
self.garden_orientation = 'north'
81+
else:
82+
self.garden_area = 0
83+
self.garden_orientation = False
84+
85+
###### ACTION ######
86+
def action_sold(self):
87+
for record in self:
88+
if record.state == 'sold':
89+
raise UserError('Property is already sold.')
90+
if record.state == 'cancelled':
91+
raise UserError('Property is cancelled and cannot be sold.')
92+
if record.state != 'offer_accepted':
93+
raise UserError('You must accept an offer before marking the property as sold.')
94+
95+
record.state = 'sold'
96+
return True
97+
98+
def action_cancel(self):
99+
for record in self:
100+
if record.state == 'cancelled':
101+
raise UserError('Property is already cancelled.')
102+
if record.state == 'sold':
103+
raise UserError('Sold properties cannot be cancelled.')
104+
105+
record.state = 'cancelled'
106+
return True
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
from dateutil.relativedelta import relativedelta
2+
from odoo import api, fields, models
3+
4+
5+
class EstatePropertyOffer(models.Model):
6+
_name = 'estate.property.offer'
7+
_description = 'An offer for a property'
8+
9+
price = fields.Float(string='Price')
10+
status = fields.Selection(
11+
string='Status',
12+
selection=[('accepted', 'Accepted'), ('refused', 'Refused'), ('pending', 'Pending')],
13+
default='pending',
14+
required=True,
15+
copy=False,
16+
)
17+
partner_id = fields.Many2one('res.partner', string='Partner', required=True)
18+
property_id = fields.Many2one('estate.property', string='Property', required=True)
19+
validity = fields.Integer(string='Validity (days)', help='Validity in days', default=7)
20+
date_deadline = fields.Date(
21+
string='Deadline',
22+
compute='_compute_date_deadline',
23+
inverse='_inverse_date_deadline',
24+
store=True,
25+
)
26+
27+
###### COMPUTE ######
28+
@api.depends('validity')
29+
def _compute_date_deadline(self):
30+
for record in self:
31+
if not record.validity:
32+
record.date_deadline = False
33+
else:
34+
record.date_deadline = fields.Date.today() + relativedelta(days=record.validity)
35+
36+
def _inverse_date_deadline(self):
37+
for record in self:
38+
if not record.date_deadline:
39+
record.validity = 0
40+
else:
41+
record.validity = (record.date_deadline - fields.Date.today()).days
42+
43+
###### ACTIONS ######
44+
def action_accept(self):
45+
for record in self:
46+
record.status = 'accepted'
47+
record.property_id.selling_price = record.price
48+
record.property_id.buyer_id = record.partner_id
49+
record.property_id.state = 'offer_accepted'
50+
51+
pending_offers = record.property_id.offer_ids.filtered(lambda x: x.status == 'pending')
52+
pending_offers.action_refuse()
53+
return True
54+
55+
def action_refuse(self):
56+
for record in self:
57+
record.status = 'refused'
58+
return True
59+
60+
def action_revert(self):
61+
for record in self:
62+
if record.status == 'accepted':
63+
record.property_id.state = 'offer_received'
64+
record.property_id.buyer_id = False
65+
record.property_id.selling_price = 0
66+
record.status = 'pending'
67+
return True

estate/models/estate_property_tag.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from random import randint
2+
3+
from odoo import fields, models
4+
5+
6+
class EstatePropertyTag(models.Model):
7+
_name = 'estate.property.tag'
8+
_description = 'Tag for categorizing estate properties'
9+
10+
def _get_default_color(self):
11+
return randint(1, 11)
12+
13+
name = fields.Char(string='Name', required=True)
14+
color = fields.Integer(string='Color', default=_get_default_color)

estate/models/estate_property_type.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from odoo import fields, models
2+
3+
4+
class EstatePropertyType(models.Model):
5+
_name = 'estate.property.type'
6+
_description = 'Type for categorizing estate properties'
7+
8+
name = fields.Char(string='Name', required=True)

estate/security/ir.model.access.csv

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
2+
estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1
3+
estate.access_estate_property_tag,access_estate_property_tag,estate.model_estate_property_tag,base.group_user,1,1,1,1
4+
estate.access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,1
5+
estate.access_estate_property_offer,access_estate_property_offer,estate.model_estate_property_offer,base.group_user,1,1,1,1

estate/views/estate_menus.xml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<odoo>
3+
<menuitem id="real_estate_root_menu" name="Real Estate" sequence="10"/>
4+
5+
<!-- Advertissements -->
6+
<menuitem id="advertissements_menu" name="Advertissements" parent="real_estate_root_menu" sequence="10">
7+
<menuitem id="advertissements_property_menu" name="Properties" action="estate_property_action" sequence="10"/>
8+
</menuitem>
9+
10+
<!-- Properties -->
11+
<menuitem id="settings_menu" name="Settings" parent="real_estate_root_menu" sequence="20">
12+
<menuitem id="settings_property_types_menu" name="Property Types" action="estate_property_types_action" sequence="10"/>
13+
<menuitem id="settings_property_tags_menu" name="Property Tags" action="estate_property_tags_action" sequence="20"/>
14+
</menuitem>
15+
</odoo>

estate/views/estate_property.xml

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<odoo>
3+
<record id="estate_property_view_list" model="ir.ui.view">
4+
<field name="name">estate.property.view.list</field>
5+
<field name="model">estate.property</field>
6+
<field name="arch" type="xml">
7+
<list string="Properties">
8+
<field name="name"/>
9+
<field name="postcode"/>
10+
<field name="bedrooms"/>
11+
<field name="living_area"/>
12+
<field name="expected_price"/>
13+
<field name="selling_price"/>
14+
<field name="date_availability"/>
15+
</list>
16+
</field>
17+
</record>
18+
19+
<record id="estate_property_view_form" model="ir.ui.view">
20+
<field name="name">estate.property.view.form</field>
21+
<field name="model">estate.property</field>
22+
<field name="arch" type="xml">
23+
<form string="Property">
24+
<header>
25+
<button name="action_sold" string="Sold" type="object" class="oe_highlight" invisible="state in ['sold', 'cancelled']"/>
26+
<button name="action_cancel" string="Cancel" type="object" invisible="state in ['sold', 'cancelled']" />
27+
<field name="state" widget="statusbar" />
28+
</header>
29+
30+
<sheet>
31+
<!-- Title -->
32+
<h1>
33+
<field name="name" class="mb16"/>
34+
</h1>
35+
<group class="mb-2">
36+
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}" nolabel="1"/>
37+
</group>
38+
39+
<!-- Main info -->
40+
<group>
41+
<group>
42+
<field name="property_type_id"/>
43+
<field name="postcode"/>
44+
<field name="date_availability"/>
45+
</group>
46+
<group>
47+
<field name="expected_price"/>
48+
<field name="best_price"/>
49+
<field name="selling_price"/>
50+
</group>
51+
</group>
52+
53+
<notebook>
54+
<!-- Description -->
55+
<page name="description" string="Description">
56+
<group>
57+
<field name="description"/>
58+
<field name="bedrooms"/>
59+
<field name="living_area"/>
60+
<field name="facades"/>
61+
<field name="garage"/>
62+
<field name="garden"/>
63+
<field name="garden_area" invisible="not garden"/>
64+
<field name="garden_orientation" invisible="not garden"/>
65+
<field name="total_area"/>
66+
</group>
67+
</page>
68+
69+
<!-- Offers -->
70+
<page name="offers" string="Offers">
71+
<field name="offer_ids"/>
72+
</page>
73+
74+
<!-- Other Info -->
75+
<page name="other_info" string="Other Info">
76+
<group>
77+
<field name="seller_id"/>
78+
<field name="buyer_id"/>
79+
</group>
80+
</page>
81+
</notebook>
82+
</sheet>
83+
</form>
84+
</field>
85+
</record>
86+
87+
<record id="estate_property_view_search" model="ir.ui.view">
88+
<field name="name">estate.property.view.search</field>
89+
<field name="model">estate.property</field>
90+
<field name="arch" type="xml">
91+
<search string="Properties">
92+
<!-- Search -->
93+
<field name="name"/>
94+
<field name="postcode"/>
95+
<field name="expected_price"/>
96+
<field name="bedrooms"/>
97+
<field name="living_area"/>
98+
<field name="facades"/>
99+
<field name="property_type_id"/>
100+
<field name="tag_ids"/>
101+
102+
<!-- Filter -->
103+
<filter name="available" string="Available" domain="[('state', 'in', ['new', 'offer_received'])]" />
104+
<filter name="available_now" string="Available Now" domain="[('state', 'in', ['new', 'offer_received']), ('date_availability', '&lt;=', context_today())]" />
105+
106+
<!-- Group -->
107+
<group name="postcode" string="Group by">
108+
<filter string="State" name="group_by_state" domain="[]" context="{'group_by': 'state'}"/>
109+
<filter string="Postcode" name="group_by_postcode" domain="[]" context="{'group_by': 'postcode'}"/>
110+
<filter string="Property Type" name="group_by_property_type" domain="[]" context="{'group_by': 'property_type_id'}"/>
111+
</group>
112+
</search>
113+
</field>
114+
</record>
115+
116+
<record id="estate_property_action" model="ir.actions.act_window">
117+
<field name="name">Estate Property Action</field>
118+
<field name="res_model">estate.property</field>
119+
<field name="view_mode">list,form</field>
120+
</record>
121+
</odoo>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<odoo>
3+
<record id="estate_property_offer_view_list" model="ir.ui.view">
4+
<field name="name">estate.property.offer.view.list</field>
5+
<field name="model">estate.property.offer</field>
6+
<field name="arch" type="xml">
7+
<list string="Offers" editable="bottom">
8+
<field name="price"/>
9+
<field name="partner_id"/>
10+
<field name="validity"/>
11+
<field name="date_deadline"/>
12+
<button name="action_accept" icon="fa-check" type="object" title="Accept" invisible="status != 'pending'"/>
13+
<button name="action_refuse" icon="fa-times" type="object" title="Refused" invisible="status != 'pending'"/>
14+
<button name="action_revert" icon="fa-undo" type="object" title="Revert" invisible="status == 'pending'"/>
15+
<field name="status" widget="badge" decoration-muted="status == 'pending'" decoration-success="status == 'accepted'" decoration-danger="status == 'refused'"/>
16+
</list>
17+
</field>
18+
</record>
19+
</odoo>

0 commit comments

Comments
 (0)