-
Notifications
You must be signed in to change notification settings - Fork 739
Technical Training Solution #59
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
5783121
6a14b59
e250921
fef90a9
cebcd80
3b50f02
f0ffe43
cb105e4
735f2b6
c81c74e
3bb3903
d4776ed
6eba645
e98e202
d3f2f32
33f5055
c687ef1
61ebad7
655a30d
0a41512
eb75686
0d012e4
3e7fa08
d47126a
6785300
332d1c7
4fa429a
32ddf63
f21fa25
d835fa2
94a5882
918a9fd
96ea962
addd5ec
c720f34
584e635
1fae471
c65e204
7c60352
dafa8a6
600ad61
3df9efc
0ccd0f3
3a10215
34e5a0b
e4bc399
2ba1884
5416861
2b0be4f
3e5c3f2
8f8d870
4123532
f580590
afa76fd
358fd5d
78565c3
80c089b
b128b85
4e99c06
c6baa17
a65af89
ef6775b
be96ad4
25ab701
1d4fa3f
d4eade1
abd410b
246fb6c
8461d70
106f2d3
0467af5
4171607
1f25ee8
8cac37d
aefcc5f
2db6fa2
d33235d
355d888
6f10575
adc43ee
5bc4d6c
5dd3026
2a24f8f
526708d
82be9e7
d0c8c00
ab3d96a
64bfdfe
160dce2
4ff9233
f7a6c15
efab55e
ffecc99
ed439f9
facc39a
e57070a
688cf00
3fb9dbe
fc2e9fa
aa69006
f1da383
d2ece76
5857125
87ad5d1
3c4f472
f545837
1e3c895
c48bfd4
327e0b9
77e7420
ce4c7b1
36bb1bd
58a276e
f129dc4
74d9d99
4871b3d
38fef5b
d2b8245
35d3a3b
3ce2b9b
c179930
3ee2df9
504635a
812c9e9
20524df
be65dfe
582a4ae
0f39b1e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,4 @@ | ||
# Odoo 12.0 - Technical Training | ||
# Odoo 17.0 - Technical Training | ||
|
||
The Technical Training of Odoo 17.0 is available on the | ||
[Tutorial](https://www.odoo.com/documentation/master/developer/howtos/rdtraining.html) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from . import models |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
{ | ||
"name": "Real Estate", # The name that will appear in the App list | ||
"version": "17.0.1.0.0", # Version | ||
"application": True, # This line says the module is an App, and not a module | ||
"depends": ["base"], # dependencies | ||
"data": [ | ||
"security/ir.model.access.csv", | ||
"views/estate_actions.xml", # add action bevor all other views, so their every time loaded before | ||
"views/estate_property_views.xml", | ||
"views/estate_property_type_views.xml", | ||
"views/estate_property_tag_views.xml", | ||
"views/estate_property_offer_views.xml", | ||
"views/estate_menus.xml" | ||
], | ||
"installable": True, | ||
'license': 'LGPL-3', | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
from . import estate_property | ||
from . import estate_property_type | ||
from . import estate_property_tag | ||
from . import estate_property_offer |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,87 @@ | ||||||||||||||||||||||||||||||
from dateutil.relativedelta import relativedelta | ||||||||||||||||||||||||||||||
from odoo import api, fields, models | ||||||||||||||||||||||||||||||
from odoo.exceptions import ValidationError | ||||||||||||||||||||||||||||||
from odoo.tools.float_utils import float_compare, float_is_zero | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
class EstateProperty(models.Model): | ||||||||||||||||||||||||||||||
_name = "estate.property" | ||||||||||||||||||||||||||||||
_description = "Estate Property" | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
name = fields.Char() | ||||||||||||||||||||||||||||||
description = fields.Text() | ||||||||||||||||||||||||||||||
postcode = fields.Char() | ||||||||||||||||||||||||||||||
date_availability = fields.Date(default=lambda self: fields.Date.today() + relativedelta(months=+3)) | ||||||||||||||||||||||||||||||
expected_price = fields.Float(required=True) | ||||||||||||||||||||||||||||||
selling_price = fields.Float(required=True) | ||||||||||||||||||||||||||||||
Comment on lines
+14
to
+15
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We haven't covered it on purpose, but for your information, in odoo there is a type of field Monetary, see https://www.odoo.com/documentation/17.0/developer/reference/backend/orm.html#odoo.fields.Monetary |
||||||||||||||||||||||||||||||
bedrooms = fields.Integer(default=2) | ||||||||||||||||||||||||||||||
living_area = fields.Integer() | ||||||||||||||||||||||||||||||
facades = fields.Integer() | ||||||||||||||||||||||||||||||
garage = fields.Boolean() | ||||||||||||||||||||||||||||||
garden = fields.Boolean() | ||||||||||||||||||||||||||||||
garden_area = fields.Integer() | ||||||||||||||||||||||||||||||
garden_orientation = fields.Selection( | ||||||||||||||||||||||||||||||
string='Type', | ||||||||||||||||||||||||||||||
selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West') ], | ||||||||||||||||||||||||||||||
help="") | ||||||||||||||||||||||||||||||
state = fields.Selection( | ||||||||||||||||||||||||||||||
string='State', | ||||||||||||||||||||||||||||||
selection=[('new', 'New'), ('offer_received', 'Offer Received'), ('offer_accepted', 'Offer Accepted'), ('sold', 'Sold'), ('canceled', 'Canceled') ], | ||||||||||||||||||||||||||||||
default='new', | ||||||||||||||||||||||||||||||
help="") | ||||||||||||||||||||||||||||||
active = fields.Boolean(default=1) | ||||||||||||||||||||||||||||||
property_type_id = fields.Many2one("estate.property.type") | ||||||||||||||||||||||||||||||
seller_id = fields.Many2one("res.partner", string="Seller") | ||||||||||||||||||||||||||||||
buyer_id = fields.Many2one("res.partner", string="Buyer") | ||||||||||||||||||||||||||||||
tag_ids = fields.Many2many("estate.property.tag", string="Tags") | ||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In most cases it is not necessary, but not that on M2m fields we can define manually the name of the table used in between the 2 models. It is especially useful when multiple m2m fields exists between the same models. See relation in In this particular case it's not an issue |
||||||||||||||||||||||||||||||
# the name of 2nd parameter "property_id" is the var string of variable used in offer model | ||||||||||||||||||||||||||||||
offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers") | ||||||||||||||||||||||||||||||
# non stored -> not searchable without search method, search="_search_totalarea" | ||||||||||||||||||||||||||||||
# store=True | ||||||||||||||||||||||||||||||
totalarea = fields.Float(compute="_compute_totalarea") | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
_sql_constraints = [ | ||||||||||||||||||||||||||||||
('check_expected_price', 'CHECK(expected_price >= 0)', 'The Expected Price should be positive.'), | ||||||||||||||||||||||||||||||
('check_selling_price', 'CHECK(selling_price >= 0)', 'The Selling Price should be positive.') | ||||||||||||||||||||||||||||||
] | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
@api.depends("living_area", "garden_area") | ||||||||||||||||||||||||||||||
def _compute_totalarea(self): | ||||||||||||||||||||||||||||||
for record in self: | ||||||||||||||||||||||||||||||
record.totalarea = record.living_area + record.garden_area | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
@api.onchange("garden") | ||||||||||||||||||||||||||||||
def _onchange_garden(self): | ||||||||||||||||||||||||||||||
if self.garden == 1: | ||||||||||||||||||||||||||||||
self.garden_area = 10 | ||||||||||||||||||||||||||||||
self.garden_orientation = "north" | ||||||||||||||||||||||||||||||
else: | ||||||||||||||||||||||||||||||
self.garden_area = 0 | ||||||||||||||||||||||||||||||
self.garden_orientation = "" | ||||||||||||||||||||||||||||||
Comment on lines
+53
to
+59
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. self.garden is a Boolean field, so self.garden == 1 will always be False
Suggested change
|
||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
def set_status_cancel(self): | ||||||||||||||||||||||||||||||
if self.state != "sold": | ||||||||||||||||||||||||||||||
self.state = "canceled" | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
def set_status_sold(self): | ||||||||||||||||||||||||||||||
if self.state != "canceled": | ||||||||||||||||||||||||||||||
self.state = "sold" | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
@api.constrains('expected_price', 'selling_price') | ||||||||||||||||||||||||||||||
def _check_selling_price(self): | ||||||||||||||||||||||||||||||
for record in self: | ||||||||||||||||||||||||||||||
# Skip the check if the selling price is zero (no offer validated yet) | ||||||||||||||||||||||||||||||
if float_is_zero(record.selling_price, precision_digits=2): | ||||||||||||||||||||||||||||||
continue | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
# Calculate 90% of the expected price | ||||||||||||||||||||||||||||||
price_limit = record.expected_price * 0.9 | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
# Compare the selling price to 90% of the expected price | ||||||||||||||||||||||||||||||
if float_compare(record.selling_price, price_limit, precision_digits=2) < 0: | ||||||||||||||||||||||||||||||
raise ValidationError("The selling price cannot be lower than 90% of the expected price.") | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
@api.ondelete(at_uninstall=False) | ||||||||||||||||||||||||||||||
def _check_state_before_deletion(self): | ||||||||||||||||||||||||||||||
for record in self: | ||||||||||||||||||||||||||||||
if record.state not in ['new', 'canceled']: | ||||||||||||||||||||||||||||||
raise ValidationError("You can only delete properties that are in 'New' or 'Canceled' state.") |
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,68 @@ | ||||||||||||||
from dateutil.relativedelta import relativedelta | ||||||||||||||
from odoo import api, fields, models | ||||||||||||||
from odoo.exceptions import UserError | ||||||||||||||
from odoo.exceptions import ValidationError | ||||||||||||||
|
||||||||||||||
|
||||||||||||||
class EstatePropertyOffer(models.Model): | ||||||||||||||
_name = "estate.property.offer" | ||||||||||||||
_description = "Estate Property Offer" | ||||||||||||||
|
||||||||||||||
name = fields.Char() | ||||||||||||||
price = fields.Float() | ||||||||||||||
status = fields.Selection( | ||||||||||||||
string='Type', | ||||||||||||||
selection=[('accepted', 'Accepted'), ('refused', 'Refused') ], | ||||||||||||||
help="") | ||||||||||||||
partner_id = fields.Many2one("res.partner", string="Partner") | ||||||||||||||
property_id = fields.Many2one("estate.property", string="Property") | ||||||||||||||
validity = fields.Integer(default=7) | ||||||||||||||
date_deadline = fields.Date(compute="_compute_date_deadline", inverse="_inverse_date_deadline", store=True) | ||||||||||||||
property_type_id = fields.Many2one(related="property_id.property_type_id", store=True) | ||||||||||||||
|
||||||||||||||
_sql_constraints = [ | ||||||||||||||
('check_price', 'CHECK(price >= 0)', 'The Offer Price should be positive.') | ||||||||||||||
] | ||||||||||||||
|
||||||||||||||
@api.depends("validity") | ||||||||||||||
def _compute_date_deadline(self): | ||||||||||||||
for record in self: | ||||||||||||||
if record.create_date: | ||||||||||||||
record.date_deadline = record.create_date + relativedelta(days=record.validity) | ||||||||||||||
else: | ||||||||||||||
record.date_deadline = fields.Date.today() + relativedelta(days=record.validity) | ||||||||||||||
Comment on lines
+30
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||
|
||||||||||||||
def _inverse_date_deadline(self): | ||||||||||||||
for record in self: | ||||||||||||||
if record.create_date: | ||||||||||||||
record.validity = (record.date_deadline - record.create_date.date()).days | ||||||||||||||
else: | ||||||||||||||
record.validity = (record.date_deadline - fields.Date.today()).days | ||||||||||||||
Comment on lines
+36
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same |
||||||||||||||
|
||||||||||||||
def estate_property_offer_accepted_action(self): | ||||||||||||||
for record in self: | ||||||||||||||
record.status = "accepted" | ||||||||||||||
record.property_id.selling_price = record.price | ||||||||||||||
record.property_id.seller_id = record.partner_id | ||||||||||||||
|
||||||||||||||
def estate_property_offer_refused_action(self): | ||||||||||||||
self.status = "refused" | ||||||||||||||
|
||||||||||||||
@api.model | ||||||||||||||
def create(self, vals): | ||||||||||||||
property_id = vals.get('property_id') | ||||||||||||||
if property_id: | ||||||||||||||
property_obj = self.env['estate.property'].browse(property_id) | ||||||||||||||
|
||||||||||||||
# Check if there is any existing offer with a higher price | ||||||||||||||
existing_offers = self.env['estate.property.offer'].search([ | ||||||||||||||
('property_id', '=', property_id), | ||||||||||||||
('price', '>', vals.get('price', 0)) | ||||||||||||||
]) | ||||||||||||||
if existing_offers: | ||||||||||||||
raise ValidationError("You cannot create an offer with a lower price than existing offers.") | ||||||||||||||
|
||||||||||||||
# Set the property state to 'Offer Received' | ||||||||||||||
property_obj.state = 'offer_received' | ||||||||||||||
|
||||||||||||||
return super(EstatePropertyOffer, self).create(vals) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
from odoo import fields, models | ||
|
||
|
||
class EstatePropertyTag(models.Model): | ||
_name = "estate.property.tag" | ||
_description = "Estate Property Tag" | ||
|
||
name = fields.Char() | ||
|
||
_sql_constraints = [ | ||
('check_name_unique', 'UNIQUE(name)', 'The name must be unique!') | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
from odoo import api, fields, models | ||
|
||
class EstatePropertyType(models.Model): | ||
_name = "estate.property.type" | ||
_description = "Estate Property Type" | ||
|
||
name = fields.Char() | ||
offer_ids = fields.One2many("estate.property.offer", "property_type_id", string="Offers") | ||
offer_count = fields.Integer(string='Offer Count', compute='_compute_offer_count') | ||
|
||
_sql_constraints = [ | ||
('check_name_unique', 'UNIQUE(name)', 'The name must be unique!') | ||
] | ||
|
||
@api.depends('offer_ids') | ||
def _compute_offer_count(self): | ||
for record in self: | ||
record.offer_count = len(record.offer_ids) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink | ||
estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 | ||
estate.access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,1 | ||
estate.access_estate_property_tag,access_estate_property_tag,estate.model_estate_property_tag,base.group_user,1,1,1,1 | ||
estate.access_estate_property_offer,access_estate_property_offer,estate.model_estate_property_offer,base.group_user,1,1,1,1 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
<odoo> | ||
<record id="action_estate_property_offer" model="ir.actions.act_window"> | ||
<field name="name">Property Offers</field> | ||
<field name="res_model">estate.property.offer</field> | ||
<field name="view_mode">tree,form</field> | ||
<field name="domain">[('property_type_id', '=', active_id)]</field> | ||
<field name="context">{'default_property_type_id': active_id}</field> | ||
</record> | ||
</odoo> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
<odoo> | ||
<menuitem id="estate_property_menu_root" name="Estate Property"> | ||
<menuitem id="estate_property0_first_level_menu" name="First Level"> | ||
<menuitem id="estate_property_00_model_menu_action" action="estate_property_model_action"/> | ||
</menuitem> | ||
<menuitem id="estate_property1_first_level_menu" name="Settings"> | ||
<menuitem id="estate_property_type_model_menu_action" action="estate_property_type_action"/> | ||
<menuitem id="estate_property_tag_model_menu_action" action="estate_property_tag_action"/> | ||
</menuitem> | ||
</menuitem> | ||
</odoo> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
<odoo> | ||
<record id="estate_property_offer_action" model="ir.actions.act_window"> | ||
<field name="name">Estate Property Offer</field> | ||
<field name="res_model">estate.property.offer</field> | ||
<field name="view_mode">tree,form</field> | ||
</record> | ||
|
||
<record id="estate_property_offer_view_tree" model="ir.ui.view"> | ||
<field name="name">estate.property.offer</field> | ||
<field name="model">estate.property.offer</field> | ||
<field name="arch" type="xml"> | ||
<tree string="Estate Property Offer"> | ||
<field name="name"/> | ||
<field name="partner_id"/> | ||
<field name="validity"/> | ||
<field name="price"/> | ||
<field name="date_deadline"/> | ||
<field name="status"/> | ||
<!-- <field name="property_type_id" invisible="1"/> <!- - make the field invisible, it do not need show to the user. leave it as learning stuff. --> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. some time it is necessary to have fields invisible just for the purpose of using them in other fields attributes, see https://www.odoo.com/documentation/17.0/developer/reference/user_interface/view_architecture.html#python-expression |
||
<button name="estate_property_offer_accepted_action" string="Accepted" type="object" icon="fa-check"/> | ||
<button name="estate_property_offer_refused_action" string="Refused" type="object" icon="fa-level-down"/> | ||
</tree> | ||
</field> | ||
</record> | ||
</odoo> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
<odoo> | ||
<record id="estate_property_tag_action" model="ir.actions.act_window"> | ||
<field name="name">Estate Property Tags</field> | ||
<field name="res_model">estate.property.tag</field> | ||
<field name="view_mode">tree,form</field> | ||
</record> | ||
</odoo> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
<odoo> | ||
<record id="estate_property_type_action" model="ir.actions.act_window"> | ||
<field name="name">Estate Property Types</field> | ||
<field name="res_model">estate.property.type</field> | ||
<field name="view_mode">tree,form</field> | ||
</record> | ||
|
||
<record id="view_estate_property_type_form" model="ir.ui.view"> | ||
<field name="name">estate.property.type.form</field> | ||
<field name="model">estate.property.type</field> | ||
<field name="arch" type="xml"> | ||
<form> | ||
<header> | ||
<button string="Offers" | ||
type="action" | ||
name="%(action_estate_property_offer)d" | ||
class="oe_stat_button" | ||
icon="fa-tags" | ||
context="{'default_property_type_id': active_id}"> | ||
<field string="Offers" name="offer_count" widget="statinfo"/> | ||
</button> | ||
</header> | ||
<field name="name" /> | ||
</form> | ||
</field> | ||
</record> | ||
|
||
|
||
</odoo> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You may also use the method defined on the field type Date: