Skip to content

Commit c61bcc4

Browse files
committed
[ADD] estate: Finish up to chapter 10 (included)
1 parent b16e643 commit c61bcc4

13 files changed

+660
-0
lines changed

estate/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import models

estate/__manifest__.py

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# A custom real estate module, created for learning purposes
2+
3+
{
4+
'name': '[TUTO] Estate',
5+
'summary': 'Track your real estate properties',
6+
'depends': [
7+
'base_setup'
8+
],
9+
'data': [
10+
'security/ir.model.access.csv',
11+
12+
'views/estate_property_views.xml',
13+
'views/estate_settings_views.xml',
14+
'views/estate_property_offer_views.xml',
15+
'views/estate_menu_views.xml'
16+
],
17+
'installable': True,
18+
'application': True,
19+
'demo': [
20+
"demo/estate_demo.xml",
21+
],
22+
'auto_install': True
23+
}

estate/demo/estate_demo.xml

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<odoo>
3+
<data>
4+
5+
<!-- Demo Estate Property Types -->
6+
<record id="estate_property_type_1" model="estate_property_type">
7+
<field name="name">Appartment</field>
8+
</record>
9+
<record id="estate_property_type_2" model="estate_property_type">
10+
<field name="name">House</field>
11+
</record>
12+
13+
<!-- Demo Estate Property Tags -->
14+
<record id="estate_property_tag_1" model="estate_property_tag">
15+
<field name="name">cosy</field>
16+
</record>
17+
<record id="estate_property_tag_2" model="estate_property_tag">
18+
<field name="name">renovated</field>
19+
</record>
20+
<record id="estate_property_tag_3" model="estate_property_tag">
21+
<field name="name">renovation needed</field>
22+
</record>
23+
24+
<!-- Demo Estate Property Offers -->
25+
<record id="estate_property_offer_1" model="estate_property_offer">
26+
<field name="price">2000000</field>
27+
<field name="partner_id" ref="base.res_partner_4"/>
28+
<field name="validity">12</field>
29+
</record>
30+
<record id="estate_property_offer_2" model="estate_property_offer">
31+
<field name="price">200000</field>
32+
<field name="partner_id" ref="base.res_partner_3"/>
33+
<field name="validity">10</field>
34+
</record>
35+
<record id="estate_property_offer_3" model="estate_property_offer">
36+
<field name="price">220000</field>
37+
<field name="partner_id" ref="base.res_partner_5"/>
38+
<field name="validity">25</field>
39+
</record>
40+
<record id="estate_property_offer_4" model="estate_property_offer">
41+
<field name="price">260000</field>
42+
<field name="partner_id" ref="base.res_partner_4"/>
43+
<field name="deadline" eval="(DateTime.today() + relativedelta(days=7)).strftime('%Y-%m-%d %H:%M')" />
44+
</record>
45+
46+
<!-- Demo Estate Properties -->
47+
<record id="estate_property_1" model="estate_property">
48+
<field name="name">Beach House</field>
49+
<field name="status">canceled</field>
50+
<field name="description">A brand new house by the beach</field>
51+
<field name="postcode">8300</field>
52+
<field name="date_availability" eval="(DateTime.today() + relativedelta(months=3)).strftime('%Y-%m-%d %H:%M')" />
53+
<field name="expected_price">2500000</field>
54+
<field name="selling_price">0</field>
55+
<field name="bedrooms">6</field>
56+
<field name="living_area">1050</field>
57+
<field name="facades">4</field>
58+
<field name="garage">True</field>
59+
<field name="garden">True</field>
60+
<field name="garden_area">420</field>
61+
<field name="garden_orientation">south</field>
62+
<field name="type_id" ref="estate_property_type_2" />
63+
<field name="salesperson" ref="base.user_demo"/>
64+
<field name="tag_ids" eval="[(6, 0, [ref('estate_property_tag_1'), ref('estate_property_tag_2')])]"/>
65+
<field name="offer_ids" eval="[(6, 0, [ref('estate_property_offer_1')])]"/>
66+
</record>
67+
<record id="estate_property_2" model="estate_property">
68+
<field name="name">City Apt</field>
69+
<field name="status">offer_accepted</field>
70+
<field name="description">A shitty appartment in Brussels</field>
71+
<field name="postcode">1090</field>
72+
<field name="date_availability" eval="(DateTime.today() + relativedelta(months=1)).strftime('%Y-%m-%d %H:%M')" />
73+
<field name="expected_price">250000</field>
74+
<field name="selling_price">0</field>
75+
<field name="bedrooms">2</field>
76+
<field name="living_area">95</field>
77+
<field name="facades">1</field>
78+
<field name="garage">False</field>
79+
<field name="garden">False</field>
80+
<field name="garden_area">0</field>
81+
<field name="type_id" ref="estate_property_type_1"/>
82+
<field name="salesperson" ref="base.user_admin"/>
83+
<field name="tag_ids" eval="[(6, 0, [ref('estate_property_tag_3')])]"/>
84+
<field name="offer_ids" eval="[(6, 0, [ref('estate_property_offer_2'), ref('estate_property_offer_4'), ref('estate_property_offer_3')])]"/>
85+
</record>
86+
87+
<!-- Mass cancel -->
88+
<record id="model_estate_property_action_cancel" model="ir.actions.server">
89+
<field name="name">Mass cancel</field>
90+
<field name="model_id" ref="estate.model_estate_property"/>
91+
<field name="binding_model_id" ref="estate.model_estate_property"/>
92+
<field name="binding_view_types">list</field>
93+
<field name="state">code</field>
94+
<field name="code">action = records.action_cancel()</field>
95+
</record>
96+
</data>
97+
</odoo>

estate/models/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import estate_property_infos, estate_property, estate_property_offer

estate/models/estate_property.py

+139
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
from odoo import api, exceptions, fields, models
2+
from datetime import date
3+
from dateutil.relativedelta import relativedelta
4+
5+
6+
class Estate_Property(models.Model):
7+
_name = "estate_property"
8+
_description = "Estate properties"
9+
_order = "id desc"
10+
active = False
11+
12+
name = fields.Char(required=True, string="Title")
13+
14+
status = fields.Selection(
15+
[
16+
("new", "New"),
17+
("offer_received", "Offer received"),
18+
("offer_accepted", "Offer Accepted"),
19+
("sold", "Sold"),
20+
("canceled", "Canceled"),
21+
],
22+
default="new",
23+
readonly=True,
24+
copy=False,
25+
string="Sale State",
26+
)
27+
28+
description = fields.Text(copy=False, string="Description")
29+
30+
postcode = fields.Char(
31+
help="Une adresse serait mieux mais bon...", string="Postcode"
32+
)
33+
34+
date_availability = fields.Date(
35+
default=(date.today() + relativedelta(months=+3)),
36+
copy=False,
37+
string="Available From",
38+
)
39+
40+
expected_price = fields.Float(
41+
default=0.0, required=True, copy=False, string="Expected Price"
42+
)
43+
44+
selling_price = fields.Float(readonly=True, string="Selling Price")
45+
46+
bedrooms = fields.Integer(default=2, string="Bedrooms")
47+
48+
living_area = fields.Integer(string="Living Area (sqm)")
49+
50+
facades = fields.Integer(default=2, string="Facades")
51+
52+
garage = fields.Boolean(default=False, string="Garage")
53+
54+
garden = fields.Boolean(default=False, string="Garden")
55+
56+
garden_area = fields.Integer(default=0, string="Garden Area (sqm)")
57+
58+
garden_orientation = fields.Selection(
59+
[("north", "North"), ("south", "South"), ("west", "West"), ("east", "East")],
60+
string="Garden Orientation",
61+
)
62+
63+
type_id = fields.Many2one(
64+
"estate_property_type", required=True, string="Property Type"
65+
)
66+
67+
buyer = fields.Many2one("res.partner", copy=False, string="Buyer")
68+
69+
salesperson = fields.Many2one(
70+
"res.users", default=(lambda self: self.env.user), string="Salesman"
71+
)
72+
73+
tag_ids = fields.Many2many("estate_property_tag", string="Property Tags")
74+
75+
offer_ids = fields.One2many("estate_property_offer", "property_id", string="Offers")
76+
77+
total_area = fields.Integer(compute="_compute_total_area", string="Total Area")
78+
79+
best_offer = fields.Float(
80+
compute="_compute_best_offer", default=0.0, string="Best Offer"
81+
)
82+
83+
_sql_constraints = [
84+
(
85+
"check_positive_expected_price",
86+
"CHECK(expected_price >= 0.0)",
87+
"Expected Price should be a positive number.",
88+
),
89+
(
90+
"check_positive_selling_price",
91+
"CHECK(selling_price >= 0.0)",
92+
"Selling Price should be a positive number.",
93+
),
94+
]
95+
96+
@api.depends("garden_area", "living_area")
97+
def _compute_total_area(self):
98+
for record in self:
99+
record.total_area = record.garden_area + record.living_area
100+
101+
@api.depends("offer_ids")
102+
def _compute_best_offer(self):
103+
for record in self:
104+
record.best_offer = max(record.offer_ids.mapped("price"))
105+
106+
@api.onchange("garden")
107+
def _onchange_garden(self):
108+
if self.garden:
109+
self.garden_area = 10
110+
self.garden_orientation = "north"
111+
else:
112+
self.garden_area = 0
113+
self.garden_orientation = ""
114+
115+
@api.constrains("selling_price", "expected_price")
116+
def _check_expected_vs_selling_price(self):
117+
for record in self:
118+
if (record.selling_price > 0.0) and (
119+
record.selling_price < 0.9 * record.expected_price
120+
):
121+
raise exceptions.ValidationError(
122+
r"Cannot sell for less than 90% of expected price."
123+
)
124+
125+
def action_sold(self):
126+
for record in self:
127+
if record.status != "canceled":
128+
record.status = "sold"
129+
else:
130+
raise exceptions.UserError("Canceled properties cannot be sold.")
131+
return True
132+
133+
def action_cancel(self):
134+
for record in self:
135+
if record.status != "sold":
136+
record.status = "canceled"
137+
else:
138+
raise exceptions.UserError("Sold properties cannot be canceled.")
139+
return True
+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from odoo import fields, models
2+
3+
4+
class Estate_Property_Type(models.Model):
5+
_name = "estate_property_type"
6+
_description = "Estate property Types"
7+
_order = "name"
8+
9+
name = fields.Char(
10+
required=True,
11+
string="Type"
12+
)
13+
14+
property_ids = fields.One2many(
15+
"estate_property",
16+
"type_id",
17+
string="Estate Properties"
18+
)
19+
20+
_sql_constraints = [
21+
("check_unique_type", "UNIQUE(name)", "Property types must be unique.")
22+
]
23+
24+
25+
class Estate_Property_Tag(models.Model):
26+
_name = "estate_property_tag"
27+
_description = "Estate property Tags"
28+
_order = "name"
29+
30+
name = fields.Char(
31+
required=True,
32+
string="Type"
33+
)
34+
35+
property_estate_ids = fields.Many2many(
36+
"estate_property",
37+
string="Estate Properties"
38+
)
39+
40+
_sql_constraints = [
41+
("check_unique_tag", "UNIQUE(name)", "Property tags must be unique.")
42+
]
+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
from odoo import api, exceptions, fields, models
2+
from datetime import date
3+
from dateutil.relativedelta import relativedelta
4+
5+
6+
class Estate_Property_Offer(models.Model):
7+
_name = "estate_property_offer"
8+
_description = "Estate Property Offers"
9+
_order = "price desc"
10+
11+
price = fields.Float(
12+
string="Price"
13+
)
14+
15+
status = fields.Selection(
16+
[("accepted", "Accepted"), ("refused", "Refused")],
17+
readonly=True,
18+
copy=False,
19+
string="Status"
20+
)
21+
22+
partner_id = fields.Many2one(
23+
'res.partner',
24+
required=True,
25+
string="Partner"
26+
)
27+
28+
property_id = fields.Many2one(
29+
'estate_property',
30+
string="Property"
31+
)
32+
33+
validity = fields.Integer(
34+
default=7,
35+
string="Validity (days)"
36+
)
37+
38+
deadline = fields.Date(
39+
compute="_compute_deadline",
40+
copy=False,
41+
string="Deadline"
42+
)
43+
44+
_sql_constraints = [
45+
("check_positive_price", "CHECK(price > 0.0)", "Offer Price should be a positive number (higher than 0).")
46+
]
47+
48+
@api.depends("validity")
49+
def _compute_deadline(self):
50+
for record in self:
51+
record.deadline = date.today() + relativedelta(days=+record.validity)
52+
53+
def _inverse_deadline(self):
54+
for record in self:
55+
record.validity = relativedelta(date.today(), record.deadline)
56+
57+
def action_accept(self):
58+
for record in self:
59+
if not any(offer_status == "accepted" for offer_status in record.property_id.offer_ids.mapped("status")):
60+
# Set values in the Property itself
61+
record.property_id.selling_price = record.price
62+
record.property_id.buyer = record.partner_id
63+
64+
record.status = "accepted"
65+
else:
66+
raise exceptions.UserError("An offer has already been accepted.")
67+
return True
68+
69+
def action_refuse(self):
70+
for record in self:
71+
if record.status == "accepted":
72+
# Set values in the Property itself
73+
record.property_id.selling_price = 0.0
74+
record.property_id.buyer = None
75+
record.status = "refused"
76+
return True

estate/security/ir.model.access.csv

+5
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_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,1
4+
estate.access_estate_property_tag,access_estate_property_tag,estate.model_estate_property_tag,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/static/description/icon.webp

9.45 KB
Binary file not shown.

0 commit comments

Comments
 (0)