Skip to content

Commit d8b1825

Browse files
committed
[IMP] estate: implement only one offer can be accepted and create invoice on property sold, do code cleanup.
This commit improves the usability of the Real Estate module by: - Making list views for Offers and Tags editable for quick updates. - Adding row coloring: refused offers appear red, accepted ones green, and the status column has been removed for visual clarity. - Enabling the ‘Available’ filter by default in property list action to surface active listings. - Modifying the living_area search input to include properties with an area greater than or equal to the entered value. - Introducing a stat button on the property type form that shows the number of linked offers. - Introduced kanban view for the properties that are on sale, grouped by property type. When a property is marked as sold, automatically create a draft customer invoice containing: - Selling price of the property. - 6% commission on the propertly selling price. - Flat $100 Administrative fees. Refactored `estate` module: - Resolved linting and indentation error. - Corrected XML ID and name for xml files according to Odoo coding guidelines. - Replaced deprecated 'kanban-box' with 'card' template. - Deleted unnecessary files. - Removed unnecessary docstrings and code comments. - Implemented the logic, that if one offer is accepted for the property then others are automatically marked as refused. - Replaced direct field assignments with `write()` method for multiple fields. - Corrected typo errors in the files. - Removed dead code that wasn't necessary.
1 parent f992186 commit d8b1825

18 files changed

+637
-42
lines changed

estate/__manifest__.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
{
2-
'name': "Real Estate",
3-
'description': """Real Estate Application.""",
4-
'summary': """Real Estate Application for beginner.""",
2+
'name': 'Real Estate',
3+
'description': 'Real Estate Application.',
4+
'summary': 'Real Estate Application for beginner.',
55
'depends': ['base'],
6-
'author': "Aaryan Parpyani (aarp)",
7-
'category': "Tutorials/RealEstate",
6+
'author': 'Aaryan Parpyani (aarp)',
7+
'category': 'Tutorials/RealEstate',
88
'version': '1.0',
99
'application': True,
1010
'installable': True,
11+
'license': 'LGPL-3',
1112
'data': [
12-
'security/ir.model.access.csv',
13-
'views/estate_menus_views.xml',
13+
'views/estate_property_offer_views.xml',
14+
'views/estate_property_type_views.xml',
15+
'views/estate_property_tags_views.xml',
16+
'views/res_users_views.xml',
1417
'views/estate_property_views.xml',
18+
'views/estate_menus_views.xml',
19+
'security/ir.model.access.csv',
1520
]
1621
}

estate/models/__init__.py

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

estate/models/estate_property.py

Lines changed: 107 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,31 @@
1-
from odoo import fields, models
1+
# imports of python lib
22
from dateutil.relativedelta import relativedelta
33

4+
# imports of odoo
5+
from odoo import api, fields, models
6+
from odoo.exceptions import UserError, ValidationError
7+
from odoo.tools.float_utils import float_compare, float_is_zero
8+
49

510
class EstateProperty(models.Model):
6-
_name = "estate.property"
7-
_description = "Estate Test"
11+
# === Private attributes ===
12+
_name = 'estate.property'
13+
_description = 'Estate Test'
14+
_order = 'id desc'
15+
16+
# SQL Constraints
17+
_sql_constraints = [
18+
('check_expected_price_positive', 'CHECK(expected_price > 0)', 'Expected price must be strictly positive.'),
19+
('check_selling_price_positive', 'CHECK(selling_price > 0)', 'Selling price must be strictly positive.'),
20+
]
821

9-
name = fields.Char(string="Name", required=True)
22+
# ---------------------
23+
# Fields declaration
24+
# ---------------------
25+
name = fields.Char(string='Name', required=True)
1026
description = fields.Char()
1127
postcode = fields.Char()
12-
date_availability = fields.Date(String="Available From", copy=False, default=fields.Date.today() + relativedelta(months=3))
28+
date_availability = fields.Date(string='Available From', copy=False, default=fields.Date.today() + relativedelta(months=3))
1329
expected_price = fields.Float(required=True)
1430
selling_price = fields.Float(readonly=True, copy=False)
1531
bedrooms = fields.Integer(default=2)
@@ -18,14 +34,17 @@ class EstateProperty(models.Model):
1834
garage = fields.Boolean()
1935
garden = fields.Boolean()
2036
garden_area = fields.Integer()
37+
active = fields.Boolean(default=True)
38+
39+
# Selection fields
2140
garden_orientation = fields.Selection(
2241
[
2342
('north', 'North'),
2443
('south', 'South'),
2544
('east', 'East'),
2645
('west', 'West')
2746
],
28-
string="Garden Orientation"
47+
string='Garden Orientation'
2948
)
3049
state = fields.Selection(
3150
[
@@ -35,8 +54,89 @@ class EstateProperty(models.Model):
3554
('sold', 'Sold'),
3655
('cancelled', 'Cancelled')
3756
],
38-
string="Status",
57+
string='Status',
3958
required=True,
4059
copy=False,
4160
default='new'
4261
)
62+
63+
# Many2one fields
64+
property_type_id = fields.Many2one('estate.property.type', string='Property Type')
65+
salesman_id = fields.Many2one('res.users', string='Salesman', default=lambda self: self.env.user)
66+
buyer_id = fields.Many2one('res.partner', string='Buyer', copy=False)
67+
68+
# Many2many fields
69+
tag_ids = fields.Many2many('estate.property.tag', string='Tags')
70+
71+
# One2many fields
72+
offer_ids = fields.One2many('estate.property.offer', 'property_id', string='Offers')
73+
74+
# Computed fields
75+
total_area = fields.Integer(string='Total Area', compute='_compute_total_area')
76+
best_price = fields.Float(string='Best Offer', compute='_compute_best_price')
77+
78+
# -----------------------
79+
# Compute methods
80+
# -----------------------
81+
@api.depends('living_area', 'garden_area')
82+
def _compute_total_area(self):
83+
for record in self:
84+
record.total_area = (record.living_area) + (record.garden_area)
85+
86+
@api.depends('offer_ids.price')
87+
def _compute_best_price(self):
88+
for record in self:
89+
if record.offer_ids:
90+
record.best_price = max(record.offer_ids.mapped('price'))
91+
else:
92+
record.best_price = 0
93+
94+
# ----------------------------------
95+
# Constraints and onchange methods
96+
# ----------------------------------
97+
@api.constrains('expected_price', 'selling_price')
98+
def _check_selling_price(self):
99+
for property in self:
100+
if not float_is_zero(property.selling_price, precision_digits=2):
101+
min_allowed = 0.9 * property.expected_price
102+
if float_compare(property.selling_price, min_allowed, precision_digits=2) < 0:
103+
raise ValidationError('Selling price cannot be lower than 90% of the expected price.')
104+
105+
@api.onchange('garden')
106+
def _onchange_garden(self):
107+
if self.garden:
108+
self.write({
109+
'garden_area': 10,
110+
'garden_orientation': 'north'
111+
})
112+
else:
113+
self.write({
114+
'garden_area': 0,
115+
'garden_orientation': False
116+
})
117+
118+
# ----------------------
119+
# CRUD methods
120+
# -----------------------
121+
@api.ondelete(at_uninstall=False)
122+
def _unlink_except_new_or_cancelled(self):
123+
for record in self:
124+
if record.state not in ['new', 'cancelled']:
125+
raise UserError('You can only delete properties that are in New or Cancelled state.')
126+
127+
# ----------------------
128+
# Action methods
129+
# ----------------------
130+
def action_mark_sold(self):
131+
for record in self:
132+
if record.state == 'cancelled':
133+
raise UserError('Cancelled properties cannot be marked as sold.')
134+
record.state = 'sold'
135+
return True
136+
137+
def action_mark_cancelled(self):
138+
for record in self:
139+
if record.state == 'sold':
140+
raise UserError('Sold properties cannot be marked as cancelled.')
141+
record.state = 'cancelled'
142+
return True
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# imports of python lib
2+
from datetime import timedelta
3+
4+
# imports of odoo
5+
from odoo import api, fields, models
6+
from odoo.exceptions import UserError
7+
from odoo.tools.float_utils import float_compare
8+
9+
10+
class EstatePropertyOffer(models.Model):
11+
# === Private attributes ===
12+
_name = 'estate.property.offer'
13+
_description = 'Offers for all the property listings.'
14+
_order = 'price desc'
15+
16+
# SQL Constraints
17+
_sql_constraints = [
18+
('check_price_positive', 'CHECK(price > 0)', 'Offer price must be strictly positive.'),
19+
]
20+
21+
# ------------------------
22+
# Fields declaration
23+
# -------------------------
24+
price = fields.Float(string='Price')
25+
validity = fields.Integer(default=7)
26+
27+
# Selection fields
28+
status = fields.Selection(
29+
[
30+
('accepted', 'Accepted'),
31+
('refused', 'Refused')
32+
],
33+
string='Status',
34+
copy=False
35+
)
36+
37+
# Computed fields
38+
date_deadline = fields.Date(compute='_compute_date_deadline', inverse='_inverse_date_deadline', store=True)
39+
40+
# Many2one fields
41+
partner_id = fields.Many2one('res.partner', required=True)
42+
property_id = fields.Many2one('estate.property', string='Property Name', required=True)
43+
property_type_id = fields.Many2one(related='property_id.property_type_id', store=True)
44+
45+
# --------------------
46+
# Compute methods
47+
# --------------------
48+
@api.depends('create_date', 'validity', 'date_deadline')
49+
def _compute_date_deadline(self):
50+
for offer in self:
51+
base_date = (offer.create_date or fields.Datetime.now()).date()
52+
offer.date_deadline = base_date + timedelta(days=offer.validity or 0)
53+
54+
def _inverse_date_deadline(self):
55+
for offer in self:
56+
base_date = (offer.create_date or fields.Datetime.now()).date()
57+
if offer.date_deadline:
58+
offer.validity = (offer.date_deadline - base_date).days
59+
60+
# ---------------------
61+
# CRUD methods
62+
# ---------------------
63+
@api.model_create_multi
64+
def create(self, vals_list):
65+
"""Overrided the default create method to enforce business rules when creating an offer.
66+
67+
Logic implemented:
68+
1. Checks if the offer amount is lower than existing offer for the property.
69+
- If so raises a UserError to prevent the creation of the offer.
70+
2. If the offer is valid, updates the related property's state to 'offer_received'.
71+
72+
Args:
73+
vals (dict): The values used to create the new estate.property.offer record.
74+
75+
Returns:
76+
recordset: the newly created estate.property.offer record.
77+
78+
Raises:
79+
UserError: If the offer amount is lower than an existing offer for the property.
80+
"""
81+
print(vals_list)
82+
for vals in vals_list:
83+
property_id = vals.get('property_id')
84+
offer_price = vals.get('price', 0.0)
85+
if not property_id or not offer_price:
86+
raise UserError('Both Property and Price must be provided.')
87+
88+
property_obj = self.env['estate.property'].browse(property_id)
89+
for offer in property_obj.offer_ids:
90+
if float_compare(offer_price, offer.price, precision_rounding=0.01) < 0:
91+
raise UserError('You cannot create an offer with an amount lower than existing offer.')
92+
93+
if property_obj.state == 'new':
94+
property_obj.state = 'offer_received'
95+
96+
return super().create(vals_list)
97+
98+
# -------------------
99+
# Action methods
100+
# -------------------
101+
def action_accept(self):
102+
"""Accept the offer and update the related property accordingly.
103+
104+
- Sets the offer's status to 'accepted'.
105+
- Sets all the offer's status to 'refused'.
106+
- Updates the property's selling price and buyer.
107+
- Updates the property's state to 'offer_accepted'.
108+
109+
Raises:
110+
UserError: If the property is already marked as 'sold'.
111+
"""
112+
for offer in self:
113+
if offer.property_id.state == 'sold':
114+
raise UserError('You cannot accept an offer for a sold property.')
115+
116+
offer.status = 'accepted'
117+
(offer.property_id.offer_ids - offer).write({'status': 'refused'})
118+
property = offer.property_id
119+
property.write({
120+
'selling_price': offer.price,
121+
'buyer_id': offer.partner_id,
122+
'state': 'offer_accepted'
123+
})
124+
return True
125+
126+
def action_refuse(self):
127+
for offer in self:
128+
offer.status = 'refused'
129+
return True

estate/models/estate_property_tag.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# imports of odoo
2+
from odoo import fields, models
3+
4+
5+
class EstatePropertyType(models.Model):
6+
# === Private attributes ===
7+
_name = 'estate.property.tag'
8+
_description = 'Property tags to represent the property.'
9+
_order = 'name'
10+
11+
# SQL Constraints
12+
_sql_constraints = [
13+
('unique_tag_name', 'UNIQUE(name)', 'Tag name must be unique.'),
14+
]
15+
16+
# === Fields declaration ===
17+
name = fields.Char('Property Tag', required=True)
18+
color = fields.Integer(string="Color")

estate/models/estate_property_type.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# imports of odoo
2+
from odoo import api, fields, models
3+
4+
5+
class EstatePropertyType(models.Model):
6+
# === Private attributes ===
7+
_name = 'estate.property.type'
8+
_description = 'Property types for available in the business.'
9+
_order = 'sequence, name'
10+
11+
# SQL Constraints
12+
_sql_constraints = [
13+
('unique_type_name', 'UNIQUE(name)', 'Type name must be unique.')
14+
]
15+
16+
# === Fields declaration ===
17+
name = fields.Char(required=True)
18+
sequence = fields.Integer('Sequence', default=10)
19+
20+
# One2many fields
21+
property_ids = fields.One2many('estate.property', 'property_type_id', string='Properties')
22+
offer_ids = fields.One2many('estate.property.offer', 'property_type_id', string='Offers')
23+
24+
# Compute fields
25+
offer_count = fields.Integer(string='Number of Offers', compute='_compute_offer_count')
26+
27+
# === Compute methods ===
28+
@api.depends('offer_ids')
29+
def _compute_offer_count(self):
30+
'''Compute the number of offers linked to each property type.
31+
32+
This method calculates the total number of related offers (`offer_ids`)
33+
for each type in the `estate.property.type` model and stores the count
34+
in the `offer_count` field.
35+
'''
36+
for record in self:
37+
record.offer_count = len(record.offer_ids) if hasattr(
38+
record, 'offer_ids') else 0

estate/models/inherited_user.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Imports of odoo
2+
from odoo import fields, models
3+
4+
5+
class InheritedUser(models.Model):
6+
# === Private attributes ===
7+
_inherit = 'res.users'
8+
9+
# === Fields declaration ===
10+
property_ids = fields.One2many('estate.property', 'salesman_id', string="Properties")

estate/security/ir.model.access.csv

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
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
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

0 commit comments

Comments
 (0)