Skip to content

Commit 4e7519c

Browse files
committed
feat: chapter 12
1 parent 8a15e6f commit 4e7519c

15 files changed

+544
-5
lines changed

estate/__manifest__.py

+9
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@
99
'depends': [
1010
'base_setup',
1111
],
12+
'data': [
13+
'data/ir.model.access.csv',
14+
'views/estate_property_views.xml',
15+
'views/estate_property_tag_views.xml',
16+
'views/estate_property_type_views.xml',
17+
'views/estate_property_offer_views.xml',
18+
'views/res_users_views.xml',
19+
'views/estate_menu.xml',
20+
],
1221
'installable': True,
1322
'application': True,
1423
'auto_install': False

estate/data/ir.model.access.csv

+5
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+
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

estate/demo/demo.xml

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<odoo>
3+
<data>
4+
<record id="model_estate_property_action_cancel" model="ir.actions.server">
5+
<field name="name">Mass cancel</field>
6+
<field name="model_id" ref="estate.model_estate_property"/>
7+
<field name="binding_model_id" ref="estate.model_estate_property"/>
8+
<field name="binding_view_types">list</field>
9+
<field name="state">code</field>
10+
<field name="code">action = records.action_cancel()</field>
11+
</record>
12+
</data>
13+
</odoo>

estate/models/__init__.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
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 res_users

estate/models/estate_property.py

+107-5
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,111 @@
1-
from odoo import models, fields
2-
1+
from odoo import api,models, fields
2+
from datetime import date
3+
from dateutil.relativedelta import relativedelta
4+
from odoo.exceptions import UserError,ValidationError
5+
from odoo.tools.float_utils import float_compare
36

47
class EstateProperty(models.Model):
5-
_name = "estate.property"
8+
_name = "estate.property"
69
_description = "Real Estate Property"
710

8-
name = fields.Char("Estate Name", required=True, translate=True)
9-
price = fields.Integer("Estate Price", default=0)
11+
name = fields.Char("Estate Name", required=True,)
12+
description = fields.Text("Description",)
13+
postcode = fields.Char("Postalcode")
14+
date_availability = fields.Date('Date Availability', default=lambda self: date.today() + relativedelta(months=3))
15+
expected_price = fields.Float('Expected Price', required=True )
16+
selling_price = fields.Float('Selling Price', readonly=True)
17+
bedrooms = fields.Integer("Bedrooms", default=2)
18+
living_area = fields.Integer("Living Area (sqm)")
19+
facades = fields.Boolean("Facades")
20+
garage = fields.Boolean("Garage")
21+
garden = fields.Boolean("Garden")
22+
garden_area = fields.Integer("Garden Area (sqm)")
23+
garden_orientation = fields.Selection( selection = [
24+
('north','NORTH'),
25+
('south','SOUTH'),
26+
('west','WEST'),
27+
('east','EAST'),
28+
])
29+
active = fields.Boolean("Active", default=True)
30+
state = fields.Selection(selection = [
31+
('new','NEW'),
32+
('offer_received','OFFER RECEIVED'),
33+
('offer_accepted','OFFER ACCEPTED'),
34+
('sold','SOLD'),
35+
('cancelled','CANCELLED'),
36+
], default='new')
37+
property_type_id = fields.Many2one('estate.property.type', string='Real Estate Type')
38+
buyer_id = fields.Many2one('res.partner', string='Buyer',)
39+
salesperson_id = fields.Many2one('res.partner', string='Salesperson', default=lambda self: self.env.user)
40+
tag_ids = fields.Many2many('estate.property.tag', string='Real Estate Tag')
41+
offer_ids = fields.One2many('estate.property.offer', inverse_name='property_id')
42+
total_area = fields.Float('Total Area (sqm)', compute='_compute_total_area', readonly=True)
43+
best_offer = fields.Float('Best Offer', compute='_compute_best_offer', readonly=True)
44+
type_id = fields.Many2one('estate.property.type')
45+
_order = 'id desc'
46+
47+
_sql_constraints = [
48+
('expected_price', 'CHECK(expected_price > 0)',
49+
'The expected price should be strictly grater than 0.'),
50+
('selling_price', 'CHECK(selling_price > 0)',
51+
'The selling price should be strictly grater than 0.'),
52+
]
53+
54+
55+
def action_open_offers(self):
56+
self.ensure_one()
57+
return {
58+
'name': 'Property Offer',
59+
'views': [(self.env.ref('estate.estate_property_offer_view_list').id, 'list')],
60+
'type': 'ir.actions.act_window',
61+
'domain' : [('id', 'in', self.offer_ids.ids)],
62+
'res_model': 'estate.property.offer',
63+
}
64+
65+
@api.ondelete(at_uninstall=False)
66+
def on_delete(self, vals_list):
67+
for val in vals_list:
68+
if val and val['state'] == 'new' or val['state'] == 'cancelled':
69+
raise UserError('You can not delete a new or cancelled property')
70+
71+
72+
@api.constrains('expected_price','selling_price')
73+
def _check_expected_price(self):
74+
for record in self:
75+
if float_compare(record.expected_price * 0.9, record.selling_price, 3) == 1 and record.selling_price != 0:
76+
raise ValidationError(f"The selling price must be at least the 90% of the expected price.")
77+
78+
79+
def action_cancel(self):
80+
if self.state != 'cancelled':
81+
self.state = 'cancelled'
82+
83+
def action_sold(self):
84+
if self.state == 'cencelled':
85+
raise UserError("You can't sold an estate marked as CANCELLED")
86+
self.state = 'sold'
87+
88+
89+
@api.depends("living_area", "garden_area")
90+
def _compute_total_area(self):
91+
for record in self:
92+
record.total_area = record.living_area + (record.garden_area or 0)
93+
94+
@api.depends("offer_ids.price")
95+
def _compute_best_offer(self):
96+
for record in self:
97+
if record.offer_ids:
98+
record.best_offer = max(offer.price for offer in record.offer_ids)
99+
else:
100+
record.best_offer = 0 # Set to 0 if no offers are present
101+
102+
@api.onchange("garden")
103+
def _onchange_garden(self):
104+
if self.garden:
105+
self.garden_area = 10
106+
self.garden_orientation = 'north'
107+
else:
108+
self.garden_area = 0 # Set to 0 when garden is False
109+
self.garden_orientation = None
110+
111+
+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
from odoo import api,models, fields
2+
from datetime import date
3+
from dateutil.relativedelta import relativedelta
4+
from odoo.exceptions import UserError
5+
from odoo.tools.float_utils import float_compare
6+
7+
class EstatePropertyOffer(models.Model):
8+
_name = "estate.property.offer"
9+
_description = "Real Estate Property Offer"
10+
11+
price = fields.Float("Offer Price", required=True,)
12+
status = fields.Selection(selection=[
13+
('accepted', 'ACCEPTED'),
14+
('refused', 'REFUSED')],)
15+
partner_id = fields.Many2one('res.partner', required=True)
16+
property_id = fields.Many2one('estate.property', required=True)
17+
validity = fields.Integer('Validity', default=7, )
18+
date_deadline = fields.Date('Date Deadline', compute='_compute_date_deadline', inverse='_inverse_date_deadline', store=True)
19+
property_type_id = fields.Many2one(
20+
"estate.property.type", related="property_id.property_type_id", string="Property Type", store=True
21+
)
22+
_order = 'price desc'
23+
24+
_sql_constraints = [
25+
('price', 'CHECK(price > 0)',
26+
'The price of an offer should be strictly grater than 0.')
27+
]
28+
29+
30+
@api.model_create_multi
31+
def create(self, vals_list):
32+
if vals_list[0].get("property_id") and vals_list[0].get("price"):
33+
prop = self.env["estate.property"].browse(vals_list[0]["property_id"])
34+
35+
if prop.offer_ids:
36+
max_offer = max(prop.mapped("offer_ids.price"))
37+
if float_compare(vals_list[0]["price"], max_offer, precision_rounding=0.01) <= 0:
38+
raise UserError("The offer must be higher than %.2f" % max_offer)
39+
prop.state = "offer_received"
40+
return super().create(vals_list)
41+
42+
43+
def action_accept(self):
44+
if self.status != 'accepted':
45+
if self.property_id.state == 'sold':
46+
raise UserError("You can't accept an offer for a property already sold")
47+
self.status = 'accepted'
48+
self.property_id.state = 'offer_accepted'
49+
self.property_id.selling_price = self.price
50+
self.property_id.buyer_id = self.partner_id.id
51+
else:
52+
raise UserError("You can't accept an offer already accepted")
53+
54+
55+
def action_refuse(self):
56+
if not self.status:
57+
self.status = 'refused'
58+
else:
59+
raise UserError("You can't refuse an offer already refused or already accepted")
60+
61+
@api.depends("create_date", "validity")
62+
def _compute_date_deadline(self):
63+
for record in self:
64+
create_date = record.create_date or fields.Date.context_today(record)
65+
record.date_deadline = create_date + relativedelta(days=record.validity)
66+
67+
def _inverse_date_deadline(self):
68+
for record in self:
69+
if record.create_date:
70+
record.validity = (record.date_deadline - record.create_date.date()).days
71+
else: # Fallback
72+
record.validity = 0
73+
74+
75+
@api.onchange('validity')
76+
def _onchange_validity(self):
77+
for record in self:
78+
if record.create_date:
79+
record.date_deadline = record.create_date + relativedelta(days=record.validity)

estate/models/estate_property_tag.py

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from odoo import models, fields
2+
from datetime import date
3+
from dateutil.relativedelta import relativedelta
4+
5+
6+
class EstatePropertyTag(models.Model):
7+
_name = "estate.property.tag"
8+
_description = "Real Estate Property Tag"
9+
10+
name = fields.Char("Estate Tag", required=True,)
11+
color = fields.Integer('Color')
12+
_order = 'name desc'
13+
14+
_sql_constraints = [
15+
('name', 'UNIQUE(name)',
16+
'The name of the tag should be unique.'),
17+
]

estate/models/estate_property_type.py

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from odoo import models, fields
2+
from datetime import date
3+
from dateutil.relativedelta import relativedelta
4+
5+
6+
class EstatePropertyType(models.Model):
7+
_name = "estate.property.type"
8+
_description = "Real Estate Property Type"
9+
10+
name = fields.Char("Estate Type", required=True,)
11+
property_ids = fields.One2many('estate.property', inverse_name='type_id')
12+
sequence = fields.Integer('Sequence', deault=1, help="Used to order types on the business needs")
13+
offer_ids = fields.One2many('estate.property.offer', inverse_name='property_type_id')
14+
_order = 'name desc'
15+
16+
_sql_constraints = [
17+
('name', 'UNIQUE(name)',
18+
'The name of the type should be unique.'),
19+
]
20+
21+
def action_open_offers(self):
22+
self.ensure_one()
23+
return {
24+
'name': 'Property Offer',
25+
'views': [(False, 'list')],
26+
'type': 'ir.actions.act_window',
27+
'domain' : [('id', 'in', self.offer_ids.ids)],
28+
'res_model': 'estate.property.offer',
29+
}

estate/models/res_users.py

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from odoo import fields, models
2+
3+
4+
class ResUsers(models.Model):
5+
6+
_inherit = "res.users"
7+
8+
property_ids = fields.One2many(
9+
"estate.property", inverse_name="salesperson_id", domain=[("state", "in", ["new", "offer_received"])]
10+
)

estate/views/estate_menu.xml

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<odoo>
3+
4+
5+
<menuitem id="estate_property_menu_root" name="Estate">
6+
<menuitem id="estate_property_menu_properties" name="Properties">
7+
<menuitem id="estate_property_sub_menu_advertisment" action="estate_property_view" name='Advertisment'/>
8+
</menuitem>
9+
<menuitem id="estate_setting_menu" name="Settings">
10+
<menuitem id="estate_settings_sub_menu_tag" action="estate_property_tag_view" name='Property Tag'/>
11+
<menuitem id="estate_settings_sub_menu_type" action="estate_property_type_view" name='Property Type'/>
12+
</menuitem>
13+
</menuitem>
14+
15+
</odoo>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<odoo>
3+
4+
<record id="estate_property_offer_view_list" model="ir.ui.view">
5+
<field name="name">estate.property.offer.view.list</field>
6+
<field name="model">estate.property.offer</field>
7+
<field name="arch" type="xml">
8+
<list editable="bottom" >
9+
<field name='price'/>
10+
<field name='partner_id'/>
11+
<field name='status'/>
12+
<button name="action_accept" invisible="status" string='Accept' type="object" icon='fa-check'/>
13+
<button name="action_refuse" invisible="status" string='Refuse' type="object" icon='fa-remove'/>
14+
</list>
15+
</field>
16+
</record>
17+
18+
19+
<!-- Action window that uses the tree view -->
20+
<record id="estate_property_offer_view" model="ir.actions.act_window">
21+
<field name="name">Properties offer</field>
22+
<field name="res_model">estate.property.offer</field>
23+
<field name="view_mode">list,search</field>
24+
</record>
25+
</odoo>
+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<odoo>
3+
4+
<record id="estate_property_tag_view_list" model="ir.ui.view">
5+
<field name="name">estate.property.tag.view.list</field>
6+
<field name="model">estate.property.tag</field>
7+
<field name="arch" type="xml">
8+
<list editable='bottom'>
9+
<field name="name" string="Title"/>
10+
</list>
11+
</field>
12+
</record>
13+
14+
<record id="estate_property_tag_view_form" model="ir.ui.view">
15+
<field name="name">estate.property.tag.view.form</field>
16+
<field name="model">estate.property.tag</field>
17+
<field name="arch" type="xml">
18+
<form string="Estate Properties Tag" >
19+
<sheet>
20+
<div class="oe_title">
21+
<h1 class="mb32 mt16 ">
22+
<field name="name" class="mb8" placeholder='Cozy, green, seaside...'/>
23+
</h1>
24+
</div>
25+
<field name="color" class="mb8" widget='color_picker'/>
26+
</sheet>
27+
</form>
28+
</field>
29+
</record>
30+
31+
32+
<!-- Action window that uses the tree view -->
33+
<record id="estate_property_tag_view" model="ir.actions.act_window">
34+
<field name="name">Properties Tag</field>
35+
<field name="res_model">estate.property.tag</field>
36+
<field name="view_mode">list,form,search</field>
37+
</record>
38+
</odoo>

0 commit comments

Comments
 (0)