Skip to content

Commit 0c71271

Browse files
committed
[IMP] estate: enhance offers and filters with actionable smart button and create invoice in property sold. Introduced kanban view.
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. This change improves automation by eliminating the need to manually create an invoice once a property is sold. It makes sure that the invoices are consistently created, reducing human error. -Resolved linting and indentation error. -Corrected XML ID and name for xml files according to Odoo coding guidelines.
1 parent 28a5ecb commit 0c71271

20 files changed

+895
-42
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
List of relations
2+
Schema | Name | Type | Owner
3+
--------+-----------------------------------------------------+-------+-------
4+
public | auth_totp_device | table | odoo
5+
public | auth_totp_wizard | table | odoo
6+
public | base_document_layout | table | odoo
7+
public | base_enable_profiling_wizard | table | odoo
8+
public | base_import_import | table | odoo
9+
public | base_import_mapping | table | odoo
10+
public | base_import_module | table | odoo
11+
public | base_language_export | table | odoo
12+
public | base_language_import | table | odoo
13+
public | base_language_install | table | odoo
14+
public | base_module_uninstall | table | odoo
15+
public | base_module_update | table | odoo
16+
public | base_module_upgrade | table | odoo
17+
public | base_partner_merge_automatic_wizard | table | odoo
18+
public | base_partner_merge_automatic_wizard_res_partner_rel | table | odoo
19+
public | base_partner_merge_line | table | odoo
20+
public | bus_bus | table | odoo
21+
public | bus_presence | table | odoo
22+
public | change_password_own | table | odoo
23+
public | change_password_user | table | odoo
24+
public | change_password_wizard | table | odoo
25+
public | decimal_precision | table | odoo
26+
public | estate_property | table | odoo
27+
public | estate_property_type | table | odoo
28+
public | iap_account | table | odoo
29+
public | iap_account_res_company_rel | table | odoo
30+
public | iap_account_res_users_rel | table | odoo
31+
public | iap_service | table | odoo
32+
public | ir_act_client | table | odoo
33+
public | ir_act_report_xml | table | odoo
34+
public | ir_act_server | table | odoo
35+
public | ir_act_server_group_rel | table | odoo
36+
public | ir_act_server_webhook_field_rel | table | odoo
37+
public | ir_act_url | table | odoo
38+
public | ir_act_window | table | odoo
39+
public | ir_act_window_group_rel | table | odoo
40+
public | ir_act_window_view | table | odoo
41+
public | ir_actions | table | odoo
42+
public | ir_actions_todo | table | odoo
43+
public | ir_asset | table | odoo
44+
public | ir_attachment | table | odoo
45+
public | ir_config_parameter | table | odoo
46+
public | ir_cron | table | odoo
47+
public | ir_cron_progress | table | odoo
48+
public | ir_cron_trigger | table | odoo
49+
public | ir_default | table | odoo
50+
public | ir_demo | table | odoo
51+
public | ir_demo_failure | table | odoo
52+
public | ir_demo_failure_wizard | table | odoo
53+
public | ir_embedded_actions | table | odoo
54+
public | ir_embedded_actions_res_groups_rel | table | odoo
55+
public | ir_exports | table | odoo
56+
public | ir_exports_line | table | odoo
57+
public | ir_filters | table | odoo
58+
public | ir_logging | table | odoo
59+
public | ir_mail_server | table | odoo
60+
public | ir_model | table | odoo
61+
public | ir_model_access | table | odoo
62+
public | ir_model_constraint | table | odoo
63+
public | ir_model_data | table | odoo
64+
public | ir_model_fields | table | odoo
65+
public | ir_model_fields_group_rel | table | odoo
66+
public | ir_model_fields_selection | table | odoo
67+
public | ir_model_inherit | table | odoo
68+
public | ir_model_relation | table | odoo
69+
public | ir_module_category | table | odoo
70+
public | ir_module_module | table | odoo
71+
public | ir_module_module_dependency | table | odoo
72+
public | ir_module_module_exclusion | table | odoo
73+
public | ir_profile | table | odoo
74+
public | ir_rule | table | odoo
75+
public | ir_sequence | table | odoo
76+
public | ir_sequence_date_range | table | odoo
77+
public | ir_ui_menu | table | odoo
78+
public | ir_ui_menu_group_rel | table | odoo
79+
public | ir_ui_view | table | odoo
80+
public | ir_ui_view_custom | table | odoo
81+
public | ir_ui_view_group_rel | table | odoo
82+
public | module_country | table | odoo
83+
public | rel_modules_langexport | table | odoo
84+
public | rel_server_actions | table | odoo
85+
public | report_layout | table | odoo
86+
public | report_paperformat | table | odoo
87+
public | res_bank | table | odoo
88+
public | res_company | table | odoo
89+
public | res_company_users_rel | table | odoo
90+
public | res_config | table | odoo
91+
public | res_config_settings | table | odoo
92+
public | res_country | table | odoo
93+
public | res_country_group | table | odoo
94+
public | res_country_res_country_group_rel | table | odoo
95+
public | res_country_state | table | odoo
96+
public | res_currency | table | odoo
97+
public | res_currency_rate | table | odoo
98+
public | res_device_log | table | odoo
99+
public | res_groups | table | odoo
100+
public | res_groups_implied_rel | table | odoo
101+
public | res_groups_report_rel | table | odoo
102+
public | res_groups_users_rel | table | odoo
103+
public | res_lang | table | odoo
104+
public | res_lang_install_rel | table | odoo
105+
public | res_partner | table | odoo
106+
public | res_partner_bank | table | odoo
107+
public | res_partner_category | table | odoo
108+
public | res_partner_industry | table | odoo
109+
public | res_partner_res_partner_category_rel | table | odoo
110+
public | res_partner_title | table | odoo
111+
public | res_users | table | odoo
112+
public | res_users_apikeys | table | odoo
113+
public | res_users_apikeys_description | table | odoo
114+
public | res_users_deletion | table | odoo
115+
public | res_users_identitycheck | table | odoo
116+
public | res_users_log | table | odoo
117+
public | res_users_settings | table | odoo
118+
public | res_users_web_tour_tour_rel | table | odoo
119+
public | reset_view_arch_wizard | table | odoo
120+
public | rule_group_rel | table | odoo
121+
public | web_editor_converter_test | table | odoo
122+
public | web_editor_converter_test_sub | table | odoo
123+
public | web_tour_tour | table | odoo
124+
public | web_tour_tour_step | table | odoo
125+
public | wizard_ir_model_menu_create | table | odoo
126+
(122 rows)
127+

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: 116 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,29 @@
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+
# === Fields declaration ===
23+
name = fields.Char(string='Name', required=True)
1024
description = fields.Char()
1125
postcode = fields.Char()
12-
date_availability = fields.Date(String="Available From", copy=False, default=fields.Date.today() + relativedelta(months=3))
26+
date_availability = fields.Date(string='Available From', copy=False, default=fields.Date.today() + relativedelta(months=3))
1327
expected_price = fields.Float(required=True)
1428
selling_price = fields.Float(readonly=True, copy=False)
1529
bedrooms = fields.Integer(default=2)
@@ -18,14 +32,17 @@ class EstateProperty(models.Model):
1832
garage = fields.Boolean()
1933
garden = fields.Boolean()
2034
garden_area = fields.Integer()
35+
active = fields.Boolean(default=True)
36+
37+
# Selection fields
2138
garden_orientation = fields.Selection(
2239
[
2340
('north', 'North'),
2441
('south', 'South'),
2542
('east', 'East'),
2643
('west', 'West')
2744
],
28-
string="Garden Orientation"
45+
string='Garden Orientation'
2946
)
3047
state = fields.Selection(
3148
[
@@ -35,8 +52,100 @@ class EstateProperty(models.Model):
3552
('sold', 'Sold'),
3653
('cancelled', 'Cancelled')
3754
],
38-
string="Status",
55+
string='Status',
3956
required=True,
4057
copy=False,
4158
default='new'
4259
)
60+
61+
# Many2one fields
62+
property_type_id = fields.Many2one('estate.property.type', string='Property Type')
63+
salesman_id = fields.Many2one('res.users', string='Salesman', default=lambda self: self.env.user)
64+
buyer_id = fields.Many2one('res.partner', string='Buyer', copy=False)
65+
66+
# Many2many fields
67+
tag_ids = fields.Many2many('estate.property.tag', string='Tags')
68+
69+
# One2many fields
70+
offer_ids = fields.One2many('estate.property.offer', 'property_id', string='Offers')
71+
72+
# Computed fields
73+
total_area = fields.Integer(string='Total Area', compute='_compute_total_area')
74+
best_price = fields.Float(string='Best Offer', compute='_compute_best_price')
75+
76+
# === Compute methods ===
77+
@api.depends('living_area', 'garden_area')
78+
def _compute_total_area(self):
79+
"""Compute the total area of the property as the sum of living and garden area."""
80+
for record in self:
81+
record.total_area = (record.living_area) + (record.garden_area)
82+
83+
@api.depends('offer_ids.price')
84+
def _compute_best_price(self):
85+
"""Compute the best (maximum) offer price for the property."""
86+
for record in self:
87+
if record.offer_ids:
88+
record.best_price = max(record.offer_ids.mapped('price'))
89+
else:
90+
record.best_price = 0
91+
92+
# === Constraints and onchange methods ===
93+
@api.constrains('expected_price', 'selling_price')
94+
def _check_selling_price(self):
95+
"""Ensures that the selling price is atleast 90% of the expected price.
96+
Raises:
97+
ValidationError: If the selling price is lower than 90% of expected price.
98+
"""
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+
"""Set the default garden area and orientation based on the 'garden' checkbox
108+
109+
- If garden is enables, defaults:
110+
- garden_area = 10
111+
- garden_orientation = 'north'
112+
- If disabled, both are cleared.
113+
"""
114+
if self.garden:
115+
self.garden_area = 10
116+
self.garden_orientation = 'north'
117+
else:
118+
self.garden_area = 0
119+
self.garden_orientation = False
120+
121+
# === CRUD methods ===
122+
@api.ondelete(at_uninstall=False)
123+
def _unlink_except_new_or_cancelled(self):
124+
"""Prevents deletion of properties unless their state is 'New' or 'Cancelled'
125+
Raises:
126+
UserError: If any record in the set has state other than 'New' or 'Cancelled'
127+
"""
128+
for record in self:
129+
if record.state not in ['new', 'cancelled']:
130+
raise UserError('You can only delete properties that are in New or Cancelled state.')
131+
132+
# === Action methods ===
133+
def action_mark_sold(self):
134+
"""Mark the property as sold.
135+
Raises:
136+
UserError: If the property is already cancelled.
137+
"""
138+
for record in self:
139+
if record.state == 'cancelled':
140+
raise UserError('Cancelled properties cannot be marked as sold.')
141+
record.state = 'sold'
142+
143+
def action_mark_cancelled(self):
144+
"""Mark the property as cancelled.
145+
Raises:
146+
UserError: If the property is already sold.
147+
"""
148+
for record in self:
149+
if record.state == 'sold':
150+
raise UserError('Sold properties cannot be marked as cancelled.')
151+
record.state = 'cancelled'

0 commit comments

Comments
 (0)