Skip to content

Commit 6436f6c

Browse files
committed
[IMP] estate: Added stat button, related fields & business logic updates
-Introduced a stat button on Property Type to display related offer count. -Restricted property deletion to New or Cancelled states, set status to Offer Received on offer creation, and prevented creation of offers lower than existing ones.
1 parent f7cddde commit 6436f6c

File tree

8 files changed

+185
-113
lines changed

8 files changed

+185
-113
lines changed

estate/__manifest__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,11 @@
77
'license': 'LGPL-3',
88
'data': [
99
'security/ir.model.access.csv',
10+
'views/estate_property_offer_views.xml',
1011
'views/estate_property_views.xml',
1112
'views/estate_property_type_views.xml',
1213
'views/estate_property_tag_views.xml',
13-
'views/estate_property_offer_views.xml',
1414
'views/estate_menus.xml'
1515
],
1616

1717
}
18-

estate/models/estate_property.py

Lines changed: 47 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from dateutil.relativedelta import relativedelta
2+
23
from odoo import models, fields, api
34
from odoo.exceptions import UserError, ValidationError
45
from odoo.tools.float_utils import float_compare, float_is_zero
@@ -7,31 +8,37 @@
78
class EstateProperty(models.Model):
89
_name = "estate.property"
910
_description = "Real Estate Property"
11+
_order = "id desc"
1012

1113
name = fields.Char(required=True)
1214
description = fields.Text()
1315
postcode = fields.Char()
16+
1417
date_availability = fields.Date(
1518
"Availability Date",
16-
default=lambda self: fields.Date.today() + relativedelta(months=3)
19+
default=lambda self: fields.Date.today() + relativedelta(months=3),
1720
)
21+
1822
expected_price = fields.Float("Expected Price", required=True)
1923
selling_price = fields.Float("Selling Price", readonly=True)
20-
bedrooms = fields.Integer(default=3)
21-
living_area = fields.Integer("Living Area (sqft)")
24+
25+
bedrooms = fields.Integer(default=2)
26+
living_area = fields.Integer("Living Area(sqft)")
2227
facades = fields.Integer()
2328
garage = fields.Boolean()
29+
2430
garden = fields.Boolean()
25-
garden_area = fields.Integer("Garden Area (sqft)")
31+
garden_area = fields.Integer("Garden Area(sqft)")
2632
garden_orientation = fields.Selection(
27-
selection=[
33+
[
2834
("north", "North"),
35+
("south", "South"),
2936
("east", "East"),
3037
("west", "West"),
31-
("South", "South"),
3238
],
3339
string="Garden Orientation",
3440
)
41+
3542
state = fields.Selection(
3643
[
3744
("new", "New"),
@@ -40,20 +47,35 @@ class EstateProperty(models.Model):
4047
("sold", "Sold"),
4148
("cancelled", "Cancelled"),
4249
],
43-
string="Status",
50+
"Status",
4451
required=True,
4552
copy=False,
4653
default="new",
4754
)
55+
4856
active = fields.Boolean(default=True)
57+
4958
property_type_id = fields.Many2one("estate.property.type", "Property Type")
5059
buyer_id = fields.Many2one("res.partner", "Buyer", copy=False)
5160
salesperson_id = fields.Many2one("res.users", string="Salesperson")
61+
5262
tag_ids = fields.Many2many("estate.property.tag", string="Tags")
53-
offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers")
54-
total_area = fields.Integer("Total Area (sqft)", compute="_compute_total_area")
63+
64+
offer_ids = fields.One2many(
65+
"estate.property.offer",
66+
"property_id",
67+
string="Offers"
68+
)
69+
70+
total_area = fields.Integer("Total Area(sqm)", compute="_compute_total_area")
5571
best_price = fields.Float("Best Offer", compute="_compute_best_price")
5672

73+
_check_expected_price = models.Constraint(
74+
'CHECK(expected_price > 0)',
75+
'The expected price of a property must be strictly positive.',
76+
)
77+
78+
5779
@api.depends("living_area", "garden_area")
5880
def _compute_total_area(self):
5981
for record in self:
@@ -62,26 +84,16 @@ def _compute_total_area(self):
6284
@api.depends("offer_ids.price")
6385
def _compute_best_price(self):
6486
for record in self:
65-
record.best_price = max(record.offer_ids.mapped("price") or [0])
87+
record.best_price = max(record.offer_ids.mapped("price"), default=0)
6688

6789
@api.onchange("garden")
6890
def _onchange_garden(self):
69-
for record in self:
70-
if record.garden:
71-
if record.garden_area == 10:
72-
record.garden_orientation = "north"
73-
elif record.garden_area == 20:
74-
record.garden_orientation = "east"
75-
elif record.garden_area == 30:
76-
record.garden_orientation = "west"
77-
elif record.garden_area == 40:
78-
record.garden_orientation = "south"
79-
else:
80-
record.garden_area = 0
81-
record.garden_orientation = False
82-
else:
83-
record.garden_area = 0
84-
record.garden_orientation = False
91+
if self.garden:
92+
self.garden_area = 10
93+
self.garden_orientation = "north"
94+
else:
95+
self.garden_area = 0
96+
self.garden_orientation = False
8597

8698
def action_cancel(self):
8799
for record in self:
@@ -95,29 +107,18 @@ def action_sold(self):
95107
raise UserError("A cancelled property cannot be set as sold")
96108
record.state = "sold"
97109

98-
_check_expected_price = models.Constraint(
99-
'CHECK(expected_price > 0)',
100-
'The expected price must be strictly positive.'
101-
)
102-
103-
_check_selling_price = models.Constraint(
104-
'CHECK(selling_price > 0)',
105-
'The selling price must be positive.'
106-
)
107-
108110
@api.constrains("selling_price", "expected_price")
109-
def _check_selling_price_ratio(self):
111+
def _check_selling_price(self):
110112
for record in self:
111113
if not float_is_zero(record.selling_price, precision_digits=2):
112-
if (
113-
float_compare(
114-
record.selling_price,
115-
record.expected_price * 0.9,
116-
precision_digits=2,
117-
)
118-
< 0
119-
):
114+
min_price = record.expected_price * 0.9
115+
if float_compare(record.selling_price, min_price, precision_digits=2) < 0:
120116
raise ValidationError(
121-
"The selling price cannot be lower then 90% of the expected price."
117+
"The selling price cannot be lower than 90% of the expected price."
122118
)
123119

120+
@api.ondelete(at_uninstall=False)
121+
def _unlink_if_new_or_cancelled(self):
122+
for record in self:
123+
if record.state not in ("new", "cancelled"):
124+
raise UserError("Only 'New' or 'Cancelled' property can be deleted.")
Lines changed: 66 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from dateutil.relativedelta import relativedelta
2-
32
from odoo import fields, models, api
43
from odoo.exceptions import UserError
54

@@ -11,41 +10,90 @@ class EstatePropertyOffer(models.Model):
1110

1211
price = fields.Float()
1312
status = fields.Selection(
14-
[("accept", "Accept"), ("refuse", "Refuse")],
13+
[("accepted", "Accepted"), ("refused", "Refused")],
1514
copy=False,
1615
)
17-
partner_id = fields.Many2one("res.partner", string="Partner", required=True)
18-
property_id = fields.Many2one("estate.property", string="Property", required=True)
19-
validity = fields.Integer(string="Validity(days)", default=15)
20-
date_deadline = fields.Date(string="Deadline Date", compute="_compute_date_deadline", inverse="_inverse_date_deadline")
16+
partner_id = fields.Many2one("res.partner", required=True)
17+
property_id = fields.Many2one("estate.property", required=True)
18+
validity = fields.Integer(default=7)
19+
date_deadline = fields.Date(
20+
string="Deadline Date",
21+
compute="_compute_date_deadline",
22+
inverse="_inverse_date_deadline"
23+
)
24+
25+
property_type_id = fields.Many2one(
26+
related="property_id.property_type_id",
27+
store=True
28+
)
2129

2230
@api.depends('validity')
2331
def _compute_date_deadline(self):
2432
for record in self:
2533
creation_date = record.create_date or fields.Date.today()
26-
record.date_deadline = relativedelta(days=record.validity) + creation_date
34+
record.date_deadline = creation_date + relativedelta(days=record.validity)
2735

2836
def _inverse_date_deadline(self):
2937
for record in self:
3038
creation_date = record.create_date or fields.Date.today()
31-
record.validity = (record.date_deadline - fields.Date.to_date(creation_date)).days
39+
record.validity = (record.date_deadline - creation_date).days
3240

3341
def action_accept(self):
3442
for record in self:
3543
if record.property_id.buyer_id:
36-
raise UserError("Property already accepted")
37-
else:
38-
record.status = 'accepted'
39-
record.property_id.selling_price = record.price
40-
record.property_id.state = 'offer_accepted'
41-
record.property_id.buyer_id = record.partner_id
44+
raise UserError("Property already has an accepted offer.")
45+
46+
record.status = 'accepted'
47+
record.property_id.selling_price = record.price
48+
record.property_id.state = 'offer_accepted'
49+
record.property_id.buyer_id = record.partner_id
4250

4351
def action_refuse(self):
44-
for record in self:
45-
record.status = 'refused'
52+
self.status = 'refused'
4653
return True
4754

55+
@api.model
56+
def create(self, vals):
57+
"""
58+
Handles:
59+
> avoid search inside loop
60+
> validate in bulk
61+
> update property state
62+
"""
63+
64+
vals_list = vals if isinstance(vals, list) else [vals]
65+
66+
property_offer_map = {}
67+
for v in vals_list:
68+
pid = v.get("property_id")
69+
if pid:
70+
property_offer_map.setdefault(pid, []).append(v)
71+
72+
for pid, offers in property_offer_map.items():
73+
prices = [o.get("price") for o in offers if o.get("price") is not None]
74+
75+
if prices:
76+
max_new_price = max(prices)
77+
78+
existing_offers = self.search([
79+
("property_id", "=", pid),
80+
("price", ">=", max_new_price),
81+
], limit=1)
82+
83+
if existing_offers:
84+
raise UserError(
85+
"You cannot create an offer with a lower amount than an existing offer for this property."
86+
)
87+
88+
records = super().create(vals)
89+
90+
if isinstance(records, models.Model):
91+
for offer in records:
92+
offer.property_id.state = "offer_received"
93+
94+
return records
95+
4896
_check_offer_price = models.Constraint(
4997
'CHECK(price > 0)',
50-
'The price must be strictly positive.'
51-
)
98+
'The price of an offer must be strictly positive.'
99+
)
Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from odoo import fields, models
1+
from odoo import fields, models, api
22

33

44
class EstatePropertyType(models.Model):
@@ -8,9 +8,16 @@ class EstatePropertyType(models.Model):
88

99
name = fields.Char(required=True)
1010
property_ids = fields.One2many("estate.property", "property_type_id", string="Properties")
11-
sequence = fields.Integer('Sequence', default=1)
11+
sequence = fields.Integer("Sequence", default=1)
12+
offer_ids = fields.One2many("estate.property.offer", "property_type_id", string="Offers")
13+
offer_count = fields.Integer(string="Offer Count", compute="_compute_offer_count")
14+
15+
@api.depends("offer_ids")
16+
def _compute_offer_count(self):
17+
for record in self:
18+
record.offer_count = len(record.offer_ids)
1219

1320
_check_type_name_unique = models.Constraint(
14-
'UNIQUE(name)',
21+
'UNIQUE(name)',
1522
'The name of the property type must be unique.'
16-
)
23+
)
Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,54 @@
11
<odoo>
2-
<record id="estate_property_offer_tree_view" model="ir.ui.view">
2+
<!-- Action -->
3+
<record id="estate_property_offer_action" model="ir.actions.act_window">
4+
<field name="name">Property Offers</field>
5+
<field name="res_model">estate.property.offer</field>
6+
<field name="view_mode">list,form</field>
7+
<field name="domain">[('property_type_id', '=', active_id)]</field>
8+
</record>
9+
10+
<!-- List View -->
11+
<record id="estate_property_offer_list_view" model="ir.ui.view">
312
<field name="name">estate.property.offer.list</field>
413
<field name="model">estate.property.offer</field>
514
<field name="arch" type="xml">
615
<list decoration-danger="status == 'refused'"
7-
decoration-success="status == 'accepted'" editable="bottom">
8-
<field name="price" />
9-
<field name="partner_id" />
10-
<field name="status" invisible="1" />
11-
<field name="validity" />
12-
<field name="date_deadline" />
13-
<button name="action_accept" type="object" string="Accept" icon="fa-check"
14-
invisible="status in ['accepted', 'refused']" />
15-
<button name="action_refuse" type="object" string="Refuse" icon="fa-times"
16-
invisible="status in ['accepted', 'refused']" />
16+
decoration-success="status == 'accepted'"
17+
editable="bottom">
18+
19+
<field name="price"/>
20+
<field name="partner_id"/>
21+
<field name="status"/>
22+
<field name="validity"/>
23+
<field name="date_deadline"/>
24+
<field name="property_type_id"/>
1725
</list>
1826
</field>
1927
</record>
2028

29+
<!-- Form View -->
2130
<record id="estate_property_offer_form_view" model="ir.ui.view">
2231
<field name="name">estate.property.offer.form</field>
2332
<field name="model">estate.property.offer</field>
2433
<field name="arch" type="xml">
2534
<form>
2635
<sheet>
2736
<group>
28-
<field name="price" />
29-
<field name="partner_id" />
30-
<field name="status" />
31-
<field name="validity" />
32-
<field name="date_deadline" />
37+
<field name="price"/>
38+
<field name="partner_id"/>
39+
<field name="status"/>
40+
<field name="validity"/>
41+
<field name="date_deadline"/>
42+
<field name="property_type_id"/>
3343
</group>
3444
</sheet>
45+
46+
<footer>
47+
<button name="action_accept" type="object" string="Accept" class="btn-primary" />
48+
<button name="action_refuse" type="object" string="Refuse" class="btn-secondary" />
49+
</footer>
3550
</form>
3651
</field>
3752
</record>
53+
3854
</odoo>

0 commit comments

Comments
 (0)