Skip to content

Commit aa107c0

Browse files
cede-odoomfreym
authored andcommitted
[ADD] estate module
closes #764 Signed-off-by: Antoine Vandevenne (anv) <[email protected]> [ADD] chapter n°1 [ADD] chapter n°2+n°3 [ADD] chapter n°4 [ADD] chapter n°5+n°6 [ADD] chapter n°7 [ADD] estate: Add a special mention section task-####### [FIX] fixed styling issues and license in the manifest [ADD] chapter n°8 [ADD] chapter n°9+n°10 [ADD] chapter n°11 to n°15
1 parent 4c650f3 commit aa107c0

File tree

89 files changed

+3801
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

89 files changed

+3801
-0
lines changed

estate_account/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import models

estate_account/__manifest__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
'name': 'Estate Account Module',
3+
'depends': ['base', 'account'],
4+
'data': [
5+
'security/ir.model.access.csv',
6+
],
7+
'license': 'LGPL-3',
8+
}

estate_account/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import estate_property
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from odoo import fields, models, api
2+
from odoo.exceptions import UserError
3+
from odoo import Command
4+
5+
6+
class EstateAccount(models.Model):
7+
_inherit = 'estate.property'
8+
9+
def action_sold(self):
10+
for property_record in self:
11+
if property_record.state == 'cancelled':
12+
raise UserError("A cancelled property cannot be set as sold")
13+
14+
partner_id = property_record.buyer_id
15+
selling_price = property_record.selling_price
16+
17+
invoice_line_1 = Command.create(
18+
{
19+
'name': 'Property Sale',
20+
'quantity': 1,
21+
'price_unit': selling_price * 1.06,
22+
},
23+
)
24+
25+
invoice_line_2 = Command.create(
26+
{
27+
'name': 'Administrative Fees',
28+
'quantity': 1,
29+
'price_unit': 100.00,
30+
},
31+
)
32+
33+
journal_id = self.env['account.journal'].search([('type', '=', 'sale')], limit=1)
34+
35+
if not journal_id:
36+
raise UserError("No sale journal found")
37+
38+
invoice_values = {
39+
'partner_id': partner_id.id,
40+
'move_type': 'out_invoice',
41+
'journal_id': journal_id.id,
42+
'invoice_date': fields.Date.today(),
43+
'invoice_line_ids': [invoice_line_1, invoice_line_2],
44+
}
45+
46+
self.env['account.move'].create(invoice_values)
47+
48+
return super().action_sold()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
'id','name','model_id:id','group_id:id','perm_read','perm_write','perm_create','perm_unlink'
2+
'model_estate_property_base_group_allow_all','model.estate.property.base.group.allow_all','model_estate_property','base.group_user',1,1,1,1

estate_property/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import models

estate_property/__manifest__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
'name': 'Estate Module',
3+
'depends': ['base'],
4+
'data': [
5+
'security/ir.model.access.csv',
6+
'views/estate_res_users.xml',
7+
'views/estate_property_offer_views.xml',
8+
'views/estate_property_type_views.xml',
9+
'views/estate_property_tag_views.xml',
10+
'views/estate_property_views.xml',
11+
'views/estate_menus.xml',
12+
],
13+
'license': 'LGPL-3',
14+
}

estate_property/models/__init__.py

Lines changed: 5 additions & 0 deletions
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
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
from datetime import date
2+
from dateutil.relativedelta import relativedelta
3+
4+
from odoo import api, fields, models
5+
from odoo.exceptions import UserError, ValidationError
6+
from odoo.tools.float_utils import float_compare, float_is_zero
7+
8+
9+
class EstateProperty(models.Model):
10+
_name = 'estate.property'
11+
_description = 'Real Estate Property'
12+
_order = 'id desc'
13+
_sql_constraints = [
14+
(
15+
'expected_price_strictly_positive',
16+
'CHECK(expected_price > 0)',
17+
'Expected price must be strictly positive',
18+
),
19+
(
20+
'selling_price_positive',
21+
'CHECK(selling_price >= 0)',
22+
'Selling price must be positive',
23+
),
24+
]
25+
26+
property_type_id = fields.Many2one(
27+
'estate.property.type',
28+
string='Property Type',
29+
)
30+
buyer_id = fields.Many2one(
31+
'res.partner',
32+
compute='_compute_infos_from_accepted_offer',
33+
string='Buyer',
34+
copy=False,
35+
)
36+
salesperson_id = fields.Many2one(
37+
'res.users',
38+
string='Salesperson',
39+
default=lambda self: self.env.user,
40+
copy=False,
41+
)
42+
tag_ids = fields.Many2many('estate.property.tag')
43+
offer_ids = fields.One2many('estate.property.offer', 'property_id', string='Offers')
44+
total_area = fields.Float(compute='_compute_total_area', string='Total Area (sqm)')
45+
best_offer = fields.Float(compute='_compute_best_offer', string='Best Offer')
46+
active = fields.Boolean(default=True)
47+
name = fields.Char(string='Title', required=True)
48+
description = fields.Text(string='Description')
49+
postcode = fields.Char(string='Postcode')
50+
date_availability = fields.Date(
51+
string='Available From',
52+
copy=False,
53+
default=date.today() + relativedelta(months=3), # noqa: DTZ011
54+
)
55+
expected_price = fields.Float(string='Expected Price', required=True)
56+
selling_price = fields.Float(
57+
compute='_compute_infos_from_accepted_offer',
58+
string='Selling Price',
59+
readonly=True,
60+
copy=False,
61+
)
62+
bedrooms = fields.Integer(string='Bedrooms', default=2)
63+
living_area = fields.Integer(string='Living Area (sqm)')
64+
facades = fields.Integer(string='Number of Facades')
65+
garage = fields.Boolean(string='Has Garage?')
66+
garden = fields.Boolean(string='Has Garden?')
67+
garden_area = fields.Integer(string='Garden Area (sqm)')
68+
garden_orientation = fields.Selection(
69+
selection=[
70+
('north', 'North'),
71+
('south', 'South'),
72+
('east', 'East'),
73+
('west', 'West'),
74+
],
75+
string='Garden Orientation',
76+
)
77+
state = fields.Selection(
78+
selection=[
79+
('new', 'New'),
80+
('offer_received', 'Offer Received'),
81+
('offer_accepted', 'Offer Accepted'),
82+
('sold', 'Sold'),
83+
('cancelled', 'Cancelled'),
84+
],
85+
string='Status',
86+
required=True,
87+
copy=False,
88+
default='new',
89+
)
90+
note = fields.Text('Special mentions about the house')
91+
92+
@api.depends('offer_ids.status')
93+
def _compute_infos_from_accepted_offer(self):
94+
for property in self:
95+
for offer in property.offer_ids:
96+
if offer.status == 'accepted':
97+
property.buyer_id = offer.partner_id
98+
property.selling_price = offer.price
99+
return
100+
property.buyer_id = None
101+
property.selling_price = None
102+
103+
@api.depends('living_area', 'garden_area')
104+
def _compute_total_area(self):
105+
for property in self:
106+
property.total_area = property.living_area + property.garden_area
107+
108+
@api.depends('offer_ids.price')
109+
def _compute_best_offer(self):
110+
for property in self:
111+
if property.offer_ids:
112+
property.best_offer = max(map(lambda r: r.price, property.offer_ids))
113+
else:
114+
property.best_offer = None
115+
116+
@api.constrains('expected_price', 'selling_price')
117+
def _check_selling_price(self):
118+
for property in self:
119+
if not float_is_zero(property.selling_price, precision_digits=2):
120+
if (
121+
float_compare(
122+
property.selling_price,
123+
property.expected_price * 0.9,
124+
precision_digits=2,
125+
)
126+
< 0
127+
):
128+
raise ValidationError("The selling price cannot be lower than 90% of the expected price!")
129+
130+
@api.onchange('garden')
131+
def _onchange_partner_id(self):
132+
if self.garden:
133+
self.garden_area = 10
134+
self.garden_orientation = 'north'
135+
else:
136+
self.garden_area = 0
137+
self.garden_orientation = None
138+
139+
@api.ondelete(at_uninstall=False)
140+
def _unlink_only_new_and_cancelled(self):
141+
if any(property.state != 'new' and property.state != 'cancelled' for property in self):
142+
raise UserError("Can't delete if state is not New or Cancelled!")
143+
144+
def action_sold(self):
145+
for property in self:
146+
if property.state != 'cancelled':
147+
property.state = 'sold'
148+
return True
149+
raise UserError("A cancelled property cannot be set as sold")
150+
151+
def action_cancel(self):
152+
for property in self:
153+
if property.state != 'sold':
154+
property.state = 'cancelled'
155+
return True
156+
raise UserError("A sold property cannot be set as cancelled")
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from datetime import timedelta
2+
3+
from odoo import api, fields, models
4+
from odoo.exceptions import ValidationError
5+
6+
7+
class EstatePropertyOffer(models.Model):
8+
_name = 'estate.property.offer'
9+
_description = 'Real Estate Property Offer'
10+
_order = 'price desc'
11+
_sql_constraints = [
12+
(
13+
'offer_price_strictly_positive',
14+
'CHECK(price > 0)',
15+
'Offer price must be strictly positive',
16+
),
17+
]
18+
19+
date_deadline = fields.Date(
20+
compute='_compute_date_deadline',
21+
inverse='_inverse_date_deadline',
22+
string='Date Deadline',
23+
)
24+
price = fields.Float()
25+
validity = fields.Integer(default=7, string='Validity (days)')
26+
status = fields.Selection(
27+
[('accepted', 'Accepted'), ('refused', 'Refused')],
28+
copy=False,
29+
)
30+
partner_id = fields.Many2one('res.partner', required=True)
31+
property_id = fields.Many2one('estate.property', required=True)
32+
property_type_id = fields.Many2one(related='property_id.property_type_id', store=True)
33+
34+
@api.depends('validity')
35+
def _compute_date_deadline(self):
36+
for record in self:
37+
record.date_deadline = (record.create_date.date() or fields.Date.now()) + timedelta(days=record.validity)
38+
39+
def _inverse_date_deadline(self):
40+
for record in self:
41+
record.validity = (record.date_deadline -(record.create_date.date() or fields.Date.now())).days
42+
43+
@api.model
44+
def create(self, vals):
45+
property_id = vals.get('property_id')
46+
47+
property = self.env['estate.property'].browse(property_id)
48+
49+
property.state = 'offer_received'
50+
51+
existing_offers = self.env['estate.property.offer'].search([('property_id', '=', property_id), ('price', '>=', vals['price'])])
52+
53+
if existing_offers:
54+
raise ValidationError("The offer value must be higher than existing offers.")
55+
56+
return super().create(vals)
57+
58+
def action_accept_offer(self):
59+
for record in self:
60+
record.status = 'accepted'
61+
62+
def action_refuse_offer(self):
63+
for record in self:
64+
record.status = 'refused'

0 commit comments

Comments
 (0)