Skip to content

Commit feb46b5

Browse files
mattosgoodjcha-odoo
authored andcommitted
[ADD] estate: add all 15 chapters covering core concepts
This commit adds the complete tutorial "Server Framework 101" covering: - Chapter 1: Odoo architecture overview and components - Chapter 2: Creating and using models - Chapter 3: Defining and working with fields - Chapter 4: Using the ORM for data manipulation - Chapter 5: Accessing and modifying data with domains and contexts - Chapter 6: Model inheritance and extension - Chapter 7: Onchange and computed fields - Chapter 8: Working with views (form, tree, search) - Chapter 9: Creating controllers and routing - Chapter 10: Implementing security (access control, record rules) - Chapter 11: Using actions and menus - Chapter 12: Working with translations and internationalization - Chapter 13: Testing (unit tests, environment setup) - Chapter 14: Creating reports - Chapter 15: Deploying and packaging modules This tutorial provides a walkthrough of Odoo server-side development concepts.
1 parent fbf9ee9 commit feb46b5

19 files changed

+842
-0
lines changed

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: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
'name': 'Real Estate',
3+
'version': '1.0',
4+
'depends': ['base'],
5+
'author': 'Jay Chauhan',
6+
'category': 'Category',
7+
'description': """
8+
Real Estate Management Module
9+
10+
This module allows managing real estate properties with detailed information including:
11+
- Property title, description, and postcode
12+
- Availability date with default scheduling
13+
- Pricing details (expected and selling price)
14+
- Property features like bedrooms, living area, facades, garage, and garden
15+
- Garden specifics including area and orientation
16+
- Status tracking through different stages: new, offer received, offer accepted, sold, cancelled
17+
- Active flag to easily archive or activate properties
18+
- User-friendly views and search with filters and group-by options for efficient property management
19+
""",
20+
'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',
26+
'security/ir.model.access.csv',
27+
],
28+
'installable': True,
29+
'application': True,
30+
'license': 'LGPL-3'
31+
}

estate/models/__init__.py

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

estate/models/estate_property.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
# Python Imports
2+
from datetime import date
3+
from dateutil.relativedelta import relativedelta
4+
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+
10+
11+
class EstateProperty(models.Model):
12+
_name = 'estate.property'
13+
_description = 'Estate Property'
14+
_order = 'id desc'
15+
16+
# -----------------------------
17+
# Field Declarations
18+
# -----------------------------
19+
name = fields.Char(string='Title', required=True, help='Title or name of the property.')
20+
description = fields.Text(string='Description', help='Detailed description of the property.')
21+
postcode = fields.Char(string='Postcode', help='Postal code of the property location.')
22+
date_availability = fields.Date(
23+
string='Availability From',
24+
copy=False,
25+
default=(date.today() + relativedelta(months=3)),
26+
help='Date from which the property will be available.'
27+
)
28+
expected_price = fields.Float(string='Expected Price', required=True, help='Price expected by the seller for this property.')
29+
selling_price = fields.Float(string='Selling Price', readonly=True, copy=False, help='Final selling price once the property is sold.')
30+
bedrooms = fields.Integer(string='Bedrooms', default=2, help='Number of bedrooms in the property.')
31+
living_area = fields.Integer(string='Living Area (sqm)', help='Living area size in square meters.')
32+
facades = fields.Integer(string='Facades', help='Number of facades of the property.')
33+
garage = fields.Integer(string='Garage', help='Number of garage spaces.')
34+
garden = fields.Boolean(string='Garden', help='Whether the property has a garden.')
35+
garden_area = fields.Integer(string='Garden Area (sqm)', help='Size of the garden area in square meters.')
36+
garden_orientation = fields.Selection(
37+
string='Garden Orientation',
38+
selection=[
39+
('north', 'North'),
40+
('south', 'South'),
41+
('east', 'East'),
42+
('west', 'West'),
43+
],
44+
default='north', help='Direction the garden faces.')
45+
state = fields.Selection(
46+
string='Status',
47+
selection=[
48+
('new', 'New'),
49+
('offer_received', 'Offer Received'),
50+
('offer_accepted', 'Offer Accepted'),
51+
('sold', 'Sold'),
52+
('cancelled', 'Cancelled'),
53+
],
54+
required=True, copy=False, default='new', help='Current status of the property.'
55+
)
56+
active = fields.Boolean(string='Active', default=True, help='Whether the property is active and visible.')
57+
property_type_id = fields.Many2one('estate.property.type', string='Property Type', help='Type or category of the property.')
58+
buyer_id = fields.Many2one('res.partner', string='Buyer', copy=False, help='Partner who bought the property.')
59+
sales_id = fields.Many2one('res.users', string='Salesman', default=lambda self: self.env.user, help='Salesperson responsible for the property.')
60+
tag_ids = fields.Many2many('estate.property.tag', string='Tags', help='Tags to classify the property.')
61+
offer_ids = fields.One2many('estate.property.offer', 'property_id', string='Offers', help='Offers made on this property.')
62+
63+
# -----------------------------
64+
# SQL Constraints
65+
# -----------------------------
66+
_sql_constraints = [
67+
('check_expected_price', 'CHECK(expected_price > 0)', 'Expected price cannot be negative.')
68+
]
69+
70+
# -----------------------------
71+
# Computed Fields
72+
# -----------------------------
73+
total = fields.Float(
74+
string='Total (sqm)',
75+
compute='_compute_total_area',
76+
help='Total area of the property including living and garden areas.'
77+
)
78+
best_price = fields.Float(
79+
string='Best Offer',
80+
compute='_compute_best_price',
81+
help='Highest offer price received for the property.'
82+
)
83+
84+
@api.depends('living_area', 'garden_area')
85+
def _compute_total_area(self):
86+
"""Compute total area as sum of living area and garden area."""
87+
for record in self:
88+
record.total = (record.living_area or 0) + (record.garden_area or 0)
89+
90+
@api.depends('offer_ids.price')
91+
def _compute_best_price(self):
92+
"""Compute highest offer price or 0 if no offers."""
93+
for record in self:
94+
offer_prices = record.offer_ids.mapped('price')
95+
record.best_price = max(offer_prices) if offer_prices else 0.0
96+
97+
# -----------------------------
98+
# Action Methods
99+
# -----------------------------
100+
def action_sold(self):
101+
"""Set property state to 'sold', with validation against invalid states."""
102+
for record in self:
103+
if record.state == 'cancelled':
104+
raise UserError('A cancelled property cannot be set as sold.')
105+
elif record.state == 'sold':
106+
raise UserError('Property is already sold.')
107+
else:
108+
record.state = 'sold'
109+
110+
def action_cancel(self):
111+
"""Set property state to 'cancelled', with validation against invalid states."""
112+
for record in self:
113+
if record.state == 'sold':
114+
raise UserError('A sold property cannot be cancelled.')
115+
elif record.state == 'cancelled':
116+
raise UserError('Property is already cancelled.')
117+
else:
118+
record.state = 'cancelled'
119+
120+
# -----------------------------
121+
# Constraints
122+
# -----------------------------
123+
@api.constrains('selling_price', 'expected_price')
124+
def _check_selling_price_above_90_percent(self):
125+
"""
126+
Validate selling price with float precision.
127+
Ignores zero selling price, otherwise enforces minimum 90% threshold.
128+
"""
129+
for record in self:
130+
if float_is_zero(record.selling_price, precision_digits=2):
131+
continue
132+
min_acceptable_price = 0.9 * record.expected_price
133+
if float_compare(record.selling_price, min_acceptable_price, precision_digits=2) < 0:
134+
raise ValidationError(_(
135+
"The selling price must be at least 90%% of the expected price.\n"
136+
"Expected Price: %(expected_price).2f\nSelling Price: %(selling_price).2f",
137+
{
138+
'expected_price': record.expected_price,
139+
'selling_price': record.selling_price
140+
}
141+
))
142+
143+
@api.ondelete(at_uninstall=False)
144+
def _check_can_be_deleted(self):
145+
"""
146+
Restrict deletion to properties in 'new' or 'cancelled' state.
147+
Raises UserError otherwise.
148+
"""
149+
for record in self:
150+
if record.state not in ['new', 'cancelled']:
151+
raise UserError('You can only delete properties that are New or Cancelled.')
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# Python Imports
2+
from datetime import timedelta
3+
4+
# Odoo Imports
5+
from odoo import _, api, fields, models
6+
from odoo.tools import float_compare
7+
from odoo.exceptions import UserError
8+
9+
10+
class EstatePropertyOffer(models.Model):
11+
_name = 'estate.property.offer'
12+
_description = 'Estate Property Offers'
13+
_order = 'price desc'
14+
15+
# -----------------------------
16+
# SQL Constraints
17+
# -----------------------------
18+
_sql_constraints = [
19+
('check_offer_price', 'CHECK(price > 0)', 'Offer price cannot be negative or zero.'),
20+
]
21+
22+
# -----------------------------
23+
# Field Declarations
24+
# -----------------------------
25+
price = fields.Float(string='Price', required=True, help='The offer price proposed by the partner.')
26+
status = fields.Selection(
27+
string='Status',
28+
selection=[
29+
('accepted', 'Accepted'),
30+
('refused', 'Refused'),
31+
],
32+
copy=False, help='Current status of the offer: Accepted or Refused.'
33+
)
34+
validity = fields.Integer(string='Validity (days)', default=7, help='Number of days this offer remains valid from the creation date.')
35+
date_deadline = fields.Date(
36+
string='Deadline', compute='_compute_date_deadline',
37+
inverse='_inverse_date_deadline',
38+
help='Deadline date until which the offer is valid.'
39+
)
40+
partner_id = fields.Many2one('res.partner', string='Partner', required=True, help='The partner who made this offer.')
41+
property_id = fields.Many2one('estate.property', string='Property', required=True, help='The property this offer is related to.')
42+
property_type_id = fields.Many2one(related='property_id.property_type_id', store=True, help='Type of the related property.')
43+
44+
# -----------------------------
45+
# Compute / Inverse Methods
46+
# -----------------------------
47+
@api.depends('create_date', 'validity')
48+
def _compute_date_deadline(self):
49+
"""
50+
Compute the deadline by adding the validity period (in days) to the creation date.
51+
Uses today's date if creation date is not available.
52+
Sets deadline to False if validity is not set.
53+
"""
54+
for record in self:
55+
create_date = record.create_date.date() if record.create_date else fields.Date.context_today(record)
56+
record.date_deadline = create_date + timedelta(days=record.validity) if record.validity else False
57+
58+
def _inverse_date_deadline(self):
59+
"""
60+
Recalculate the validity period based on the difference between the deadline
61+
and the creation date (or today's date if creation date is missing).
62+
Validity is set to zero if no deadline is specified.
63+
"""
64+
for record in self:
65+
create_date = record.create_date.date() if record.create_date else fields.Date.context_today(record)
66+
if record.date_deadline:
67+
delta = record.date_deadline - create_date
68+
record.validity = max(delta.days, 0)
69+
else:
70+
record.validity = 0
71+
72+
# -----------------------------
73+
# CRUD Methods
74+
# -----------------------------
75+
@api.model_create_multi
76+
def create(self, vals_list):
77+
"""
78+
Override create to validate offer before creation:
79+
- Ensure property and price are provided.
80+
- Prevent creating offers lower than existing offers.
81+
- Update property state if it's 'new'.
82+
"""
83+
for vals in vals_list:
84+
property_id = vals.get('property_id')
85+
offer_price = vals.get('price', 0.0)
86+
if not property_id or not offer_price:
87+
raise UserError(_('Both property and price must be provided.'))
88+
89+
Property = self.env['estate.property'].browse(property_id)
90+
91+
for offer in Property.offer_ids:
92+
if float_compare(offer_price, offer.price, precision_rounding=0.01) < 0:
93+
raise UserError(_('Cannot create an offer lower than an existing offer.'))
94+
95+
if Property.state == 'new':
96+
Property.state = 'offer_received'
97+
98+
# Pass all valid vals to super
99+
return super().create(vals_list)
100+
101+
# -----------------------------
102+
# Action Methods
103+
# -----------------------------
104+
def action_confirm(self):
105+
"""
106+
Confirm the offer:
107+
- Set offer status to 'accepted'.
108+
- Update related property status and selling details.
109+
"""
110+
self.ensure_one()
111+
for record in self:
112+
record.status = 'accepted'
113+
record.property_id.state = 'offer_accepted'
114+
record.property_id.selling_price = record.price
115+
record.property_id.buyer_id = record.partner_id
116+
117+
(self.property_id.offer_ids - record).write({'status': 'refused'})
118+
119+
def action_refuse(self):
120+
"""
121+
Refuse the offer by setting its status to 'refused'.
122+
"""
123+
self.ensure_one()
124+
for record in self:
125+
record.status = 'refused'

estate/models/estate_property_tag.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Odoo Imports
2+
from odoo import fields, models
3+
4+
5+
class EstatePropertyTag(models.Model):
6+
_name = 'estate.property.tag'
7+
_description = 'Estate Property Tags'
8+
_order = 'name'
9+
10+
# -----------------------------
11+
# Fields
12+
# -----------------------------
13+
name = fields.Char(string='Property Tag', required=True, help='Name of the tag used to categorize or label properties.')
14+
color = fields.Integer(string='Color', help='Color code used to visually distinguish this tag.')
15+
16+
# -----------------------------
17+
# SQL Constraints
18+
# -----------------------------
19+
_sql_constraints = [
20+
('uniq_tag_name', 'unique(name)', 'Tag name must be unique.'),
21+
]

estate/models/estate_property_type.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Odoo Imports
2+
from odoo import api, fields, models
3+
4+
5+
class EstatePropertyType(models.Model):
6+
_name = 'estate.property.type'
7+
_description = 'Estate Property Type'
8+
_order = 'sequence, name'
9+
10+
# -----------------------------
11+
# Fields
12+
# -----------------------------
13+
name = fields.Char(string='Property Type', required=True, help='Name of the property type (e.g., Apartment, House).')
14+
property_ids = fields.One2many('estate.property', 'property_type_id', string='Properties', help='Properties categorized under this type.')
15+
offer_ids = fields.One2many('estate.property.offer', 'property_type_id', string='Offers', help='Offers associated with properties of this type.')
16+
sequence = fields.Integer(string='Sequence', default=10, help='Used to order property types in lists and views.')
17+
offer_count = fields.Integer(string='Number of Offers', compute='_compute_offer_count', help='Total number of offers made on properties of this type.')
18+
19+
# -----------------------------
20+
# SQL Constraints
21+
# -----------------------------
22+
_sql_constraints = [
23+
('uniq_property_type_name', 'UNIQUE(name)', 'Property type name must be unique.'),
24+
]
25+
26+
# -----------------------------
27+
# Compute Function
28+
# -----------------------------
29+
@api.depends('offer_ids')
30+
def _compute_offer_count(self):
31+
"""
32+
Compute the total number of offers associated with this property type.
33+
"""
34+
for record in self:
35+
record.offer_count = len(record.offer_ids) if hasattr(
36+
record, 'offer_ids') else 0

estate/models/inherited_user.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Odoo Imports
2+
from odoo import fields, models
3+
4+
5+
class InheritedUser(models.Model):
6+
_inherit = 'res.users'
7+
8+
# -----------------------------
9+
# Field Declarations
10+
# -----------------------------
11+
property_ids = fields.One2many(
12+
'estate.property',
13+
'sales_id',
14+
string='Properties',
15+
domain=[('state', 'in', ['new', 'offer_received'])],
16+
help='Properties assigned to this salesman with status New or Offer Received.'
17+
)

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+
access_estate_property,access.estate.property,model_estate_property,base.group_user,1,1,1,1
3+
access_estate_property_type,access.estate.property.type,model_estate_property_type,base.group_user,1,1,1,1
4+
access_estate_property_tag,access.estate.property.tag,model_estate_property_tag,base.group_user,1,1,1,1
5+
access_estate_property_offer,access.estate.property.offer,model_estate_property_offer,base.group_user,1,1,1,1
22 KB
Loading

0 commit comments

Comments
 (0)