Skip to content

Commit 2fe6a7a

Browse files
committed
[ADD] estate: add chapters 5–15 of Server Framework 101
This commit adds the core sections of the Server Framework 101 tutorial, from chapter 5 (Add a model) to chapter 15 (Add security groups). These chapters walk through progressively building a functional Odoo module, including defining models, views, menus, access controls, security groups, and testing setups. They serve as an educational reference for new developers learning to develop server-side features in Odoo. Included topics: - Creating models and fields - Defining views (form, tree) - Adding menus and actions - Configuring access rights and record rules - Organizing data files and dependencies - Writing and loading test data - Creating security groups and assigning permissions This is part of a full tutorial module and does not include production-ready features, but aims to demonstrate development best practices.
1 parent e3dfb9d commit 2fe6a7a

19 files changed

+932
-173
lines changed

estate/__manifest__.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
2-
'name': "Real Estate",
2+
'name': 'Real Estate',
33
'version': '1.0',
44
'depends': ['base'],
5-
'author': "Jay Chauhan",
5+
'author': 'Jay Chauhan',
66
'category': 'Category',
77
'description': """
88
Real Estate Management Module
@@ -18,10 +18,14 @@
1818
- User-friendly views and search with filters and group-by options for efficient property management
1919
""",
2020
'data': [
21+
'views/estate_property_offer_view.xml',
22+
'views/estate_property_type_view.xml',
23+
'views/estate_property_tag_view.xml',
24+
'views/estate_property_view.xml',
25+
'views/estate_menu.xml',
2126
'security/ir.model.access.csv',
22-
'views/estate_menus.xml',
23-
'views/estate_property_views.xml'
2427
],
2528
'installable': True,
26-
'application': True
29+
'application': True,
30+
'license': 'LGPL-3'
2731
}

estate/models/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
from . import estate_property
1+
from . import estate_property, estate_property_type, estate_property_tag, estate_property_offer, inherited_user

estate/models/estate_property.py

Lines changed: 208 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,239 @@
1-
from odoo import models, fields
1+
# Python Imports
22
from datetime import date
33
from dateutil.relativedelta import relativedelta
44

5+
# Odoo Imports
6+
from odoo import _, api, fields, models
7+
from odoo.exceptions import UserError, ValidationError
8+
from odoo.tools.float_utils import float_compare, float_is_zero
9+
510

611
class EstateProperty(models.Model):
712
_name = 'estate.property'
813
_description = 'Estate Property'
14+
_order = 'id desc'
915

10-
# Title of the property (required)
11-
name = fields.Char(string='Title', required=True)
12-
13-
# Detailed description text
14-
description = fields.Text(string='Description')
15-
16-
# Postcode as a simple Char field
17-
postcode = fields.Char(string='Postcode')
18-
19-
# Availability date with default 3 months from today; copy=False avoids duplication on record duplication
16+
# -----------------------------
17+
# Field Declarations
18+
# -----------------------------
19+
name = fields.Char(
20+
string='Title',
21+
required=True,
22+
help='Title or name of the property.'
23+
)
24+
description = fields.Text(
25+
string='Description',
26+
help='Detailed description of the property.'
27+
)
28+
postcode = fields.Char(
29+
string='Postcode',
30+
help='Postal code of the property location.'
31+
)
2032
date_availability = fields.Date(
2133
string='Availability From',
2234
copy=False,
23-
default=(date.today() + relativedelta(months=3))
35+
default=(date.today() + relativedelta(months=3)),
36+
help='Date from which the property will be available.'
37+
)
38+
expected_price = fields.Float(
39+
string='Expected Price',
40+
required=True,
41+
help='Price expected by the seller for this property.'
42+
)
43+
selling_price = fields.Float(
44+
string='Selling Price',
45+
readonly=True,
46+
copy=False,
47+
help='Final selling price once the property is sold.'
48+
)
49+
bedrooms = fields.Integer(
50+
string='Bedrooms',
51+
default=2,
52+
help='Number of bedrooms in the property.'
53+
)
54+
living_area = fields.Integer(
55+
string='Living Area (sqm)',
56+
help='Living area size in square meters.'
57+
)
58+
facades = fields.Integer(
59+
string='Facades',
60+
help='Number of facades of the property.'
61+
)
62+
garage = fields.Integer(
63+
string='Garage',
64+
help='Number of garage spaces.'
65+
)
66+
garden = fields.Boolean(
67+
string='Garden',
68+
help='Whether the property has a garden.'
69+
)
70+
garden_area = fields.Integer(
71+
string='Garden Area (sqm)',
72+
help='Size of the garden area in square meters.'
2473
)
25-
26-
# Expected sale price (required)
27-
expected_price = fields.Float(string='Expected Price', required=True)
28-
29-
# Actual selling price, read-only (set by system or workflow), not copied on duplication
30-
selling_price = fields.Float(string='Selling Price', readonly=True, copy=False)
31-
32-
# Number of bedrooms with default value
33-
bedrooms = fields.Integer(string='Bedrooms', default=2)
34-
35-
# Living area in square meters
36-
living_area = fields.Integer(string='Living Area (sqm)')
37-
38-
# Number of facades
39-
facades = fields.Integer(string='Facades')
40-
41-
# Garage capacity count
42-
garage = fields.Integer(string='Garage')
43-
44-
# Boolean indicating if garden exists
45-
garden = fields.Boolean(string='Garden')
46-
47-
# Garden area in square meters
48-
garden_area = fields.Integer(string='Garden Area (sqm)')
49-
50-
# Orientation of the garden with default 'north'
5174
garden_orientation = fields.Selection(
75+
string='Garden Orientation',
5276
selection=[
5377
('north', 'North'),
5478
('south', 'South'),
5579
('east', 'East'),
56-
('west', 'West')
80+
('west', 'West'),
5781
],
58-
string='Garden Orientation',
59-
default='north'
82+
default='north',
83+
help='Direction the garden faces.'
6084
)
61-
62-
# Status of the property; required, default 'new', not copied on duplication
6385
state = fields.Selection(
86+
string='Status',
6487
selection=[
6588
('new', 'New'),
6689
('offer_received', 'Offer Received'),
6790
('offer_accepted', 'Offer Accepted'),
6891
('sold', 'Sold'),
6992
('cancelled', 'Cancelled'),
7093
],
71-
string='Status',
7294
required=True,
7395
copy=False,
74-
default='new'
96+
default='new',
97+
help='Current status of the property.'
98+
)
99+
active = fields.Boolean(
100+
string='Active',
101+
default=True,
102+
help='Whether the property is active and visible.'
103+
)
104+
property_type_id = fields.Many2one(
105+
'estate.property.type',
106+
string='Property Type',
107+
help='Type or category of the property.'
108+
)
109+
buyer_id = fields.Many2one(
110+
'res.partner',
111+
string='Buyer',
112+
copy=False,
113+
help='Partner who bought the property.'
114+
)
115+
sales_id = fields.Many2one(
116+
'res.users',
117+
string='Salesman',
118+
default=lambda self: self.env.user,
119+
help='Salesperson responsible for the property.'
120+
)
121+
tag_ids = fields.Many2many(
122+
'estate.property.tag',
123+
string='Tags',
124+
help='Tags to classify the property.'
125+
)
126+
offer_ids = fields.One2many(
127+
'estate.property.offer',
128+
'property_id',
129+
string='Offers',
130+
help='Offers made on this property.'
131+
)
132+
133+
# -----------------------------
134+
# SQL Constraints
135+
# -----------------------------
136+
_sql_constraints = [
137+
('check_expected_price', 'CHECK(expected_price > 0)', 'Expected price cannot be negative.'),
138+
('check_selling_price', 'CHECK(selling_price > 0)', 'Selling price cannot be negative.'),
139+
]
140+
141+
# -----------------------------
142+
# Computed Fields
143+
# -----------------------------
144+
total = fields.Float(
145+
string='Total (sqm)',
146+
compute='_compute_total_area',
147+
help='Total area of the property including living and garden areas.'
148+
)
149+
best_price = fields.Float(
150+
string='Best Offer',
151+
compute='_compute_best_price',
152+
help='Highest offer price received for the property.'
75153
)
76154

77-
# Active flag to archive/unarchive records easily
78-
active = fields.Boolean(string='Active', default=True)
155+
@api.depends('living_area', 'garden_area')
156+
def _compute_total_area(self):
157+
"""Compute total area as sum of living area and garden area."""
158+
for record in self:
159+
record.total = (record.living_area or 0) + (record.garden_area or 0)
160+
161+
@api.depends('offer_ids.price')
162+
def _compute_best_price(self):
163+
"""Compute highest offer price or 0 if no offers."""
164+
for record in self:
165+
offer_prices = record.offer_ids.mapped('price')
166+
record.best_price = max(offer_prices) if offer_prices else 0.0
167+
168+
# -----------------------------
169+
# Action Methods
170+
# -----------------------------
171+
def action_sold(self):
172+
"""Set property state to 'sold', with validation against invalid states."""
173+
for record in self:
174+
if record.state == 'cancelled':
175+
raise UserError('A cancelled property cannot be set as sold.')
176+
elif record.state == 'sold':
177+
raise UserError('Property is already sold.')
178+
else:
179+
record.state = 'sold'
180+
181+
def action_cancel(self):
182+
"""Set property state to 'cancelled', with validation against invalid states."""
183+
for record in self:
184+
if record.state == 'sold':
185+
raise UserError('A sold property cannot be cancelled.')
186+
elif record.state == 'cancelled':
187+
raise UserError('Property is already cancelled.')
188+
else:
189+
record.state = 'cancelled'
190+
191+
# -----------------------------
192+
# Constraints
193+
# -----------------------------
194+
@api.constrains('selling_price', 'expected_price')
195+
def _check_selling_price(self):
196+
"""
197+
Ensure selling price is at least 90% of expected price.
198+
Raises ValidationError if condition is not met.
199+
"""
200+
for record in self:
201+
if record.selling_price and record.selling_price < 0.9 * record.expected_price:
202+
raise ValidationError(
203+
_("The selling price must be at least 90%% of the expected price.\n"
204+
"Expected Price: %(expected).2f\nSelling Price: %(selling).2f")
205+
% {
206+
'expected': record.expected_price,
207+
'selling': record.selling_price
208+
}
209+
)
210+
211+
@api.constrains('selling_price', 'expected_price')
212+
def _check_selling_price_above_90_percent(self):
213+
"""
214+
Validate selling price with float precision.
215+
Ignores zero selling price, otherwise enforces minimum 90% threshold.
216+
"""
217+
for record in self:
218+
if float_is_zero(record.selling_price, precision_digits=2):
219+
continue
220+
min_acceptable_price = 0.9 * record.expected_price
221+
if float_compare(record.selling_price, min_acceptable_price, precision_digits=2) < 0:
222+
raise ValidationError(
223+
_("The selling price must be at least 90%% of the expected price.\n"
224+
"Expected Price: %(expected_price).2f\nSelling Price: %(selling_price).2f")
225+
% {
226+
'expected_price': record.expected_price,
227+
'selling_price': record.selling_price
228+
}
229+
)
230+
231+
@api.ondelete(at_uninstall=False)
232+
def _check_can_be_deleted(self):
233+
"""
234+
Restrict deletion to properties in 'new' or 'cancelled' state.
235+
Raises UserError otherwise.
236+
"""
237+
for record in self:
238+
if record.state not in ['new', 'cancelled']:
239+
raise UserError("You can only delete properties that are New or Cancelled.")

0 commit comments

Comments
 (0)