Skip to content

Commit 54edce7

Browse files
author
raibr
committed
[FIX] estate: replace invalid models.Constraint with _sql_constraints
The models.Constraint syntax does not exist in Odoo's ORM. SQL constraints must be defined using the _sql_constraints class attribute.
1 parent 90bef7f commit 54edce7

11 files changed

+185
-124
lines changed

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,3 @@ dmypy.json
127127

128128
# Pyre type checker
129129
.pyre/
130-
.DS_Store

README-raibr-docs.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

estate/__manifest__.py

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
# Part of Odoo. See LICENSE file for full copyright and licensing details.
22
{
3-
'name': 'Real Estate Advertisement',
4-
'version': '1.0',
5-
'author': 'Odoo',
6-
'category': 'Sales/Real Estate',
7-
'sequence': 15,
8-
'summary': 'Manage property listings and real estate advertisements',
9-
'description': """
3+
"name": "Real Estate Advertisement",
4+
"author": "Odoo",
5+
"category": "Sales/Real Estate",
6+
"sequence": 15,
7+
"summary": "Manage property listings and real estate advertisements",
8+
"description": """
109
Real Estate Advertisement Management
1110
====================================
1211
This module allows you to manage real estate properties, including:
@@ -15,16 +14,15 @@
1514
* Property offers and negotiations
1615
* Sales tracking
1716
""",
18-
'depends': ['base'],
19-
'data': [
20-
'security/ir.model.access.csv',
21-
'views/estate_property_views.xml',
22-
'views/estate_property_type_views.xml',
23-
'views/estate_property_tag_views.xml',
24-
'views/estate_property_offer_views.xml',
25-
'views/estate_menus.xml',
17+
"depends": ["base"],
18+
"data": [
19+
"security/ir.model.access.csv",
20+
"views/estate_property_views.xml",
21+
"views/estate_property_type_views.xml",
22+
"views/estate_property_tag_views.xml",
23+
"views/estate_property_offer_views.xml",
24+
"views/estate_menus.xml",
2625
],
27-
'installable': True,
28-
'application': True,
29-
'license': 'LGPL-3',
26+
"application": True,
27+
"license": "LGPL-3",
3028
}

estate/models/estate_property.py

Lines changed: 62 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,73 @@
11
# Part of Odoo. See LICENSE file for full copyright and licensing details.
22

33
from odoo import api, fields, models
4-
from odoo.exceptions import UserError
4+
from odoo.exceptions import UserError, ValidationError
5+
from odoo.tools.float_utils import float_compare, float_is_zero
56

67

78
class EstateProperty(models.Model):
8-
_name = "estate.property"
9+
_name = 'estate.property'
910
_description = "Real Estate Property"
11+
_order = 'id desc'
12+
_check_expected_price = models.Constraint('Check(expected_price > 0)', "The expected price must be strictly positive.")
13+
_check_selling_price = models.Constraint('Check(selling_price >= 0)', "The selling price must be positive.")
1014

11-
name = fields.Char(required=True)
12-
description = fields.Text()
13-
postcode = fields.Char()
14-
date_availability = fields.Date()
15-
expected_price = fields.Float()
16-
selling_price = fields.Float()
17-
bedrooms = fields.Integer()
18-
living_area = fields.Integer()
19-
facades = fields.Integer()
20-
garage = fields.Boolean()
21-
garden = fields.Boolean()
22-
garden_area = fields.Integer()
15+
name = fields.Char(required=True, string="Name")
16+
description = fields.Text(string="Description")
17+
postcode = fields.Char(string="Postcode")
18+
date_availability = fields.Date(string="Available From")
19+
expected_price = fields.Float(required=True, string="Expected Price")
20+
selling_price = fields.Float(string="Selling Price")
21+
bedrooms = fields.Integer(string="Bedrooms")
22+
living_area = fields.Integer(string="Living Area (sqm)")
23+
facades = fields.Integer(string="Facades")
24+
garage = fields.Boolean(string="Garage")
25+
garden = fields.Boolean(string="Garden")
26+
garden_area = fields.Integer(string="Garden Area (sqm)")
2327
garden_orientation = fields.Selection(
2428
selection=[
25-
('north', 'North'),
26-
('south', 'South'),
27-
('east', 'East'),
28-
('west', 'West')
29-
]
29+
('north', "North"),
30+
('south', "South"),
31+
('east', "East"),
32+
('west', "West"),
33+
],
34+
string="Garden Orientation",
3035
)
3136
state = fields.Selection(
3237
selection=[
33-
('new', 'New'),
34-
('offer_received', 'Offer Received'),
35-
('offer_accepted', 'Offer Accepted'),
36-
('sold', 'Sold'),
37-
('cancelled', 'Cancelled')
38+
('new', "New"),
39+
('offer_received', "Offer Received"),
40+
('offer_accepted', "Offer Accepted"),
41+
('sold', "Sold"),
42+
('cancelled', "Cancelled"),
3843
],
3944
default='new',
40-
required=True
45+
required=True,
46+
string="Status",
47+
)
48+
buyer_id = fields.Many2one(comodel_name='res.partner', string="Buyer")
49+
property_type_id = fields.Many2one(comodel_name='estate.property.type', string="Property Type")
50+
tag_ids = fields.Many2many(comodel_name='estate.property.tag', string="Tags")
51+
offer_ids = fields.One2many(
52+
comodel_name='estate.property.offer',
53+
inverse_name='property_id',
54+
string="Offers",
4155
)
42-
buyer_id = fields.Many2one('res.partner', string='Buyer')
43-
property_type_id = fields.Many2one("estate.property.type", string="Property Type")
44-
tag_ids = fields.Many2many("estate.property.tag", string="Tags")
45-
offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers")
46-
total_area = fields.Integer(compute="_compute_total_area")
47-
best_price = fields.Float(compute="_compute_best_price", string="Best Offer Price")
56+
total_area = fields.Integer(compute='_compute_total_area', string="Total Area (sqm)")
57+
best_price = fields.Float(compute='_compute_best_price', string="Best Offer Price")
4858

4959
@api.depends('living_area', 'garden_area')
5060
def _compute_total_area(self):
51-
for record in self:
52-
record.total_area = record.living_area + record.garden_area
61+
for property in self:
62+
property.total_area = property.living_area + property.garden_area
5363

5464
@api.depends('offer_ids.price')
5565
def _compute_best_price(self):
56-
for record in self:
57-
if record.offer_ids:
58-
record.best_price = max(record.offer_ids.mapped('price'))
66+
for property in self:
67+
if property.offer_ids:
68+
property.best_price = max(property.offer_ids.mapped('price'))
5969
else:
60-
record.best_price = 0.0
70+
property.best_price = 0.0
6171

6272
@api.onchange('garden')
6373
def _onchange_garden(self):
@@ -68,16 +78,23 @@ def _onchange_garden(self):
6878
self.garden_area = 0
6979
self.garden_orientation = False
7080

81+
@api.constrains('selling_price', 'expected_price')
82+
def _check_selling_price(self):
83+
for property in self:
84+
if not float_is_zero(property.selling_price, precision_digits=2):
85+
if float_compare(property.selling_price, property.expected_price * 0.9, precision_digits=2) < 0:
86+
raise ValidationError(self.env._("The selling price cannot be lower than 90% of the expected price."))
87+
7188
def action_sold(self):
72-
for record in self:
73-
if record.state == 'cancelled':
74-
raise UserError("Cancelled property cannot be sold.")
75-
record.state = 'sold'
89+
for property in self:
90+
if property.state == 'cancelled':
91+
raise UserError(self.env._("Cancelled property cannot be sold."))
92+
property.state = 'sold'
7693
return True
7794

7895
def action_cancel(self):
79-
for record in self:
80-
if record.state == 'sold':
81-
raise UserError("Sold property cannot be cancelled.")
82-
record.state = 'cancelled'
96+
for property in self:
97+
if property.state == 'sold':
98+
raise UserError(self.env._("Sold property cannot be cancelled."))
99+
property.state = 'cancelled'
83100
return True

estate/models/estate_property_offer.py

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,48 +6,52 @@
66

77

88
class EstatePropertyOffer(models.Model):
9-
_name = "estate.property.offer"
9+
_name = 'estate.property.offer'
1010
_description = "Real Estate Property Offer"
11-
_order = "price desc"
12-
13-
price = fields.Float(required=True)
14-
status = fields.Selection(
15-
selection=[
16-
('accepted', 'Accepted'),
17-
('refused', 'Refused')
18-
],
19-
copy=False
20-
)
21-
partner_id = fields.Many2one('res.partner', string='Partner', required=True)
22-
property_id = fields.Many2one("estate.property", string="Property", required=True)
11+
_order = 'price desc'
12+
13+
price = fields.Float(required=True, string="Price")
14+
_check_price = models.Constraint('Check(price > 0)', "The offer price must be strictly positive.")
15+
status = fields.Selection(selection=[('accepted', "Accepted"), ('refused', "Refused")], copy=False, string="Status")
16+
partner_id = fields.Many2one(comodel_name='res.partner', string="Partner", required=True)
17+
property_id = fields.Many2one(comodel_name='estate.property', string="Property", required=True)
2318
validity = fields.Integer(default=7, string="Validity (days)")
2419
date_deadline = fields.Date(
25-
compute="_compute_date_deadline",
26-
inverse="_inverse_date_deadline",
20+
compute='_compute_date_deadline',
21+
inverse='_inverse_date_deadline',
2722
store=True,
2823
string="Deadline",
2924
)
3025

31-
@api.depends("validity")
26+
# self.property_id.status = 'offer_received' when offer is created
27+
@api.model
28+
def create(self, vals):
29+
offer = super().create(vals)
30+
if offer.property_id.state == 'new':
31+
offer.property_id.state = 'offer_received'
32+
return offer
33+
34+
@api.depends('validity')
3235
def _compute_date_deadline(self):
33-
for record in self:
34-
record.date_deadline = fields.Date.today() + timedelta(days=record.validity)
36+
for offer in self:
37+
offer.date_deadline = fields.Date.today() + timedelta(days=offer.validity)
3538

3639
def _inverse_date_deadline(self):
37-
for record in self:
38-
if record.date_deadline:
39-
record.validity = (record.date_deadline - fields.Date.today()).days
40+
for offer in self:
41+
if offer.date_deadline:
42+
offer.validity = (offer.date_deadline - fields.Date.today()).days
4043

4144
def action_accept(self):
42-
for record in self:
43-
if record.property_id.offer_ids.filtered(lambda o: o.status == 'accepted' and o.id != record.id):
44-
raise UserError("Only one offer can be accepted per property.")
45-
record.status = 'accepted'
46-
record.property_id.buyer_id = record.partner_id
47-
record.property_id.selling_price = record.price
45+
for offer in self:
46+
if offer.property_id.offer_ids.filtered(lambda o: o.status == 'accepted' and o.id != offer.id):
47+
raise UserError(self.env._("Only one offer can be accepted per property."))
48+
offer.status = 'accepted'
49+
offer.property_id.buyer_id = offer.partner_id
50+
offer.property_id.selling_price = offer.price
51+
offer.property_id.state = 'offer_accepted'
4852
return True
4953

5054
def action_refuse(self):
51-
for record in self:
52-
record.status = 'refused'
55+
for offer in self:
56+
offer.status = 'refused'
5357
return True

estate/models/estate_property_tag.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44

55

66
class EstatePropertyTag(models.Model):
7-
_name = "estate.property.tag"
7+
_name = 'estate.property.tag'
88
_description = "Real Estate Property Tag"
9-
_order = "name"
9+
_order = 'name'
1010

11-
name = fields.Char(required=True)
11+
_check_unique_name = models.Constraint('UNIQUE(name)', "The tag name must be unique.")
12+
13+
name = fields.Char(required=True, string="Name")
14+
color = fields.Integer(string="Color")
Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
# Part of Odoo. See LICENSE file for full copyright and licensing details.
22

3-
from odoo import fields, models
3+
from odoo import api, fields, models
44

55

66
class EstatePropertyType(models.Model):
7-
_name = "estate.property.type"
7+
_name = 'estate.property.type'
88
_description = "Real Estate Property Type"
9-
_order = "name"
9+
_order = 'name'
10+
_check_unique_name = models.Constraint('UNIQUE(name)', "The type name must be unique.")
1011

11-
name = fields.Char(required=True)
12+
name = fields.Char(required=True, string="Name")
13+
sequence = fields.Integer(default=1, string="Sequence")
14+
property_ids = fields.One2many(comodel_name='estate.property', inverse_name='property_type_id', string="Properties")
15+
offer_count = fields.Integer(compute='_compute_offer_count', string="Offer Count")
16+
17+
def _compute_offer_count(self):
18+
for property_type in self:
19+
property_type.offer_count = sum(property_type.property_ids.mapped(lambda p: len(p.offer_ids)))

estate/views/estate_property_offer_views.xml

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<odoo>
3-
<!-- Tree View -->
4-
<record id="estate_property_offer_view_tree" model="ir.ui.view">
5-
<field name="name">estate.property.offer.view.tree</field>
3+
<!-- List View -->
4+
<record id="estate_property_offer_view_list" model="ir.ui.view">
5+
<field name="name">estate.property.offer.view.list</field>
66
<field name="model">estate.property.offer</field>
77
<field name="arch" type="xml">
8-
<list>
8+
<list editable="bottom" decoration-danger="status == 'refused'" decoration-success="status == 'accepted'">
99
<field name="price"/>
1010
<field name="partner_id"/>
11+
<field name="property_id"/>
1112
<field name="validity"/>
1213
<field name="date_deadline"/>
13-
<field name="status"/>
14+
<field name="status" column_invisible="1"/>
1415
<button name="action_accept" type="object" icon="fa-check" title="Accept"/>
1516
<button name="action_refuse" type="object" icon="fa-times" title="Refuse"/>
17+
1618
</list>
1719
</field>
1820
</record>
@@ -42,5 +44,6 @@
4244
<field name="name">Property Offers</field>
4345
<field name="res_model">estate.property.offer</field>
4446
<field name="view_mode">list,form</field>
47+
<field name="domain">[('property_id.property_type_id', '=', active_id)]</field>
4548
</record>
4649
</odoo>

estate/views/estate_property_tag_views.xml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<odoo>
33
<!-- List view -->
4-
<record id="estate_property_tag_view_tree" model="ir.ui.view">
5-
<field name="name">estate.property.tag.view.tree</field>
4+
<record id="estate_property_tag_view_list" model="ir.ui.view">
5+
<field name="name">estate.property.tag.view.list</field>
66
<field name="model">estate.property.tag</field>
77
<field name="arch" type="xml">
8-
<list string="Property Tags">
8+
<list string="Property Tags" editable="bottom">
99
<field name="name"/>
1010
</list>
1111
</field>

0 commit comments

Comments
 (0)