-
Notifications
You must be signed in to change notification settings - Fork 3.1k
yoelf - technical training #1244
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: 19.0
Are you sure you want to change the base?
Changes from all commits
84fe695
77632cf
88e8f53
f5b54a2
b0f3765
0dc462d
2538c7d
22ff6ee
895799e
a80712f
74913fe
10f341c
0ad3838
8295f65
3427faa
ccd4483
f8a83a9
f8c5799
40db478
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 |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| from . import models |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| { | ||
| "name": "estate", | ||
| "depends": [ | ||
| "base", | ||
| ], | ||
| "application": True, | ||
| "author": "Yoelf", | ||
| "license": "LGPL-3", | ||
| "version": "19.0.0.1.0", | ||
| "data": [ | ||
| "security/ir.model.access.csv", | ||
| "views/estate_property_views.xml", | ||
| "views/estate_property_offer_views.xml", | ||
| "views/estate_property_type_views.xml", | ||
| "views/estate_property_tag_views.xml", | ||
| "views/res_user_views.xml", | ||
| "views/estate_menus.xml", | ||
| ], | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| from . import estate_property | ||
| from . import estate_property_type | ||
| from . import estate_property_tag | ||
| from . import estate_property_offer | ||
| from . import res_user |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,128 @@ | ||||||||||||||
| from odoo.exceptions import UserError, ValidationError | ||||||||||||||
| from odoo.tools.float_utils import float_compare, float_is_zero | ||||||||||||||
|
|
||||||||||||||
| from odoo import api, fields, models | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| class EstateProperty(models.Model): | ||||||||||||||
| _name = "estate.property" | ||||||||||||||
| _description = "Estate Property" | ||||||||||||||
| _order = "id desc" | ||||||||||||||
|
|
||||||||||||||
| name = fields.Char(required=True) | ||||||||||||||
|
yoelfatihi marked this conversation as resolved.
|
||||||||||||||
| description = fields.Text() | ||||||||||||||
| postcode = fields.Char() | ||||||||||||||
| date_availability = fields.Date( | ||||||||||||||
| copy=False, default=lambda self: fields.Date.add(fields.Date.today(), months=3) | ||||||||||||||
| ) | ||||||||||||||
|
yoelfatihi marked this conversation as resolved.
|
||||||||||||||
| expected_price = fields.Integer(required=True) | ||||||||||||||
| selling_price = fields.Integer(readonly=True, copy=False) | ||||||||||||||
| bedrooms = fields.Integer(default=2) | ||||||||||||||
| living_area = fields.Integer(default=0) | ||||||||||||||
| facades = fields.Integer() | ||||||||||||||
| garage = fields.Boolean() | ||||||||||||||
| garden = fields.Boolean() | ||||||||||||||
| garden_area = fields.Integer(default=0) | ||||||||||||||
| garden_orientation = fields.Selection( | ||||||||||||||
| string="orientation", | ||||||||||||||
| selection=[ | ||||||||||||||
| ("north", "North"), | ||||||||||||||
| ("south", "South"), | ||||||||||||||
| ("east", "East"), | ||||||||||||||
| ("west", "West"), | ||||||||||||||
| ], | ||||||||||||||
| ) | ||||||||||||||
| active = fields.Boolean(default=True) | ||||||||||||||
| state = fields.Selection( | ||||||||||||||
| string="State", | ||||||||||||||
| selection=[ | ||||||||||||||
| ("new", "New"), | ||||||||||||||
| ("offer_received", "Offer Received"), | ||||||||||||||
| ("offer_accepted", "Offer Accepted"), | ||||||||||||||
| ("sold", "Sold"), | ||||||||||||||
| ("cancelled", "Cancelled"), | ||||||||||||||
| ], | ||||||||||||||
| required=True, | ||||||||||||||
| default="new", | ||||||||||||||
| ) | ||||||||||||||
| estate_property_type_id = fields.Many2one( | ||||||||||||||
| "estate.property.type", string="Property Type" | ||||||||||||||
| ) | ||||||||||||||
|
Comment on lines
+48
to
+50
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. If you name the field 'property_type_id' you won't need to add the string attribute :) |
||||||||||||||
| buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False, readonly=True) | ||||||||||||||
|
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. Won't say it anymore ater that but there are more instances: For many2one field the default |
||||||||||||||
| salesperson_id = fields.Many2one( | ||||||||||||||
| "res.users", string="Salesman", default=lambda self: self.env.user | ||||||||||||||
| ) | ||||||||||||||
|
yoelfatihi marked this conversation as resolved.
|
||||||||||||||
| estate_property_tag_ids = fields.Many2many("estate.property.tag", string="Tags") | ||||||||||||||
| estate_property_offer_ids = fields.One2many("estate.property.offer", "property_id") | ||||||||||||||
| total_area = fields.Integer(compute="_compute_total_area") | ||||||||||||||
| best_price = fields.Float(compute="_compute_best_price") | ||||||||||||||
|
|
||||||||||||||
| _check_expected_price_positive = models.Constraint( | ||||||||||||||
| "CHECK(expected_price > 0)", | ||||||||||||||
| "A property expected price must be strictly positive.", | ||||||||||||||
| ) | ||||||||||||||
| _check_selling_price_positive = models.Constraint( | ||||||||||||||
| "CHECK(selling_price > 0)", "A property selling price must be positive" | ||||||||||||||
| ) | ||||||||||||||
|
|
||||||||||||||
| @api.depends("living_area", "garden_area", "garden") | ||||||||||||||
| def _compute_total_area(self): | ||||||||||||||
| for property in self: | ||||||||||||||
| property.total_area = ( | ||||||||||||||
| property.living_area + property.garden_area | ||||||||||||||
| if property.garden | ||||||||||||||
| else property.living_area | ||||||||||||||
| ) | ||||||||||||||
|
|
||||||||||||||
| @api.depends("estate_property_offer_ids.price") | ||||||||||||||
| def _compute_best_price(self): | ||||||||||||||
| for property in self: | ||||||||||||||
| property.best_price = ( | ||||||||||||||
| max(property.estate_property_offer_ids.mapped("price")) | ||||||||||||||
| if property.estate_property_offer_ids | ||||||||||||||
| else None | ||||||||||||||
| ) | ||||||||||||||
|
Comment on lines
+80
to
+84
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
|
||||||||||||||
|
|
||||||||||||||
| @api.onchange("garden") | ||||||||||||||
| def _onchange_has_garden(self): | ||||||||||||||
| if self.garden: | ||||||||||||||
| self.garden_area = 10 | ||||||||||||||
| self.garden_orientation = "north" | ||||||||||||||
|
Comment on lines
+86
to
+90
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. Here you forgot to reset to the default value if garden is false |
||||||||||||||
|
|
||||||||||||||
| def action_sell(self): | ||||||||||||||
| for property in self: | ||||||||||||||
| if property.state == "cancelled": | ||||||||||||||
| raise UserError("You can't sell a a cancelled property :)") | ||||||||||||||
|
|
||||||||||||||
| property.state = "sold" | ||||||||||||||
| return True | ||||||||||||||
|
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. return True is indented too much here, will return at the first iteration of the loop |
||||||||||||||
|
|
||||||||||||||
| def action_cancel(self): | ||||||||||||||
| for property in self: | ||||||||||||||
| if property.state == "sold": | ||||||||||||||
| raise UserError("you can't cancel a sold property :)") | ||||||||||||||
|
|
||||||||||||||
| property.state = "cancelled" | ||||||||||||||
| return True | ||||||||||||||
|
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 |
||||||||||||||
|
|
||||||||||||||
| @api.constrains("selling_price", "expected_price") | ||||||||||||||
| def _check_expected_to_selling(self): | ||||||||||||||
| for property in self: | ||||||||||||||
| if ( | ||||||||||||||
| not float_is_zero(property.selling_price, precision_digits=2) | ||||||||||||||
| ) and float_compare( | ||||||||||||||
| property.selling_price, | ||||||||||||||
| property.expected_price * 0.9, | ||||||||||||||
| precision_digits=2, | ||||||||||||||
| ) == -1: | ||||||||||||||
| raise ValidationError( | ||||||||||||||
| r"the selling price cannot be lower than 90% of the expected price" | ||||||||||||||
|
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. Why a raw string here ? |
||||||||||||||
| ) | ||||||||||||||
|
|
||||||||||||||
| @api.ondelete(at_uninstall=False) | ||||||||||||||
| def on_delete(self): | ||||||||||||||
| for property in self: | ||||||||||||||
| if property.state not in ["new", "cancelled"]: | ||||||||||||||
| raise UserError( | ||||||||||||||
| f"You cannot delete a property at the {property.state} state" | ||||||||||||||
| ) | ||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,88 @@ | ||||||||||||||||||||||||||||
| from odoo.exceptions import UserError | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| from odoo import api, fields, models | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| class EstatePropertyOffer(models.Model): | ||||||||||||||||||||||||||||
| _name = "estate.property.offer" | ||||||||||||||||||||||||||||
| _description = "Offer for property" | ||||||||||||||||||||||||||||
| _order = "price desc" | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| price = fields.Float() | ||||||||||||||||||||||||||||
| state = fields.Selection( | ||||||||||||||||||||||||||||
| string="Status", | ||||||||||||||||||||||||||||
| selection=[("accepted", "Accepted"), ("refused", "Refused")], | ||||||||||||||||||||||||||||
| copy=False, | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| partner_id = fields.Many2one("res.partner", required=True) | ||||||||||||||||||||||||||||
| property_id = fields.Many2one("estate.property", required=True) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| validity = fields.Integer(default=7) | ||||||||||||||||||||||||||||
| date_deadline = fields.Date( | ||||||||||||||||||||||||||||
| compute="_compute_date_deadline", inverse="_inverse_date_deadline" | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| property_type_id = fields.Many2one( | ||||||||||||||||||||||||||||
| "estate.property.type", | ||||||||||||||||||||||||||||
| related="property_id.estate_property_type_id", | ||||||||||||||||||||||||||||
| store=True, | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| _check_offer_price_positive = models.Constraint( | ||||||||||||||||||||||||||||
| "CHECK(price > 0)", "The offer price must be positive." | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| @api.depends("validity") | ||||||||||||||||||||||||||||
| def _compute_date_deadline(self): | ||||||||||||||||||||||||||||
| for offer in self: | ||||||||||||||||||||||||||||
| if offer.create_date: | ||||||||||||||||||||||||||||
| offer.date_deadline = fields.Date.add( | ||||||||||||||||||||||||||||
| offer.create_date, | ||||||||||||||||||||||||||||
| days=offer.validity, | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||
| offer.date_deadline = fields.Date.add( | ||||||||||||||||||||||||||||
| fields.Date.today(), days=offer.validity | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
|
Comment on lines
+37
to
+45
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 offer in self: | ||||||||||||||||||||||||||||
| if offer.create_date: | ||||||||||||||||||||||||||||
| offer.validity = (offer.date_deadline - offer.create_date.date()).days | ||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||
| offer.validity = (offer.date_deadline - fields.Date.today()).days | ||||||||||||||||||||||||||||
|
Comment on lines
+48
to
+52
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 action_accept_offer(self): | ||||||||||||||||||||||||||||
| for offer in self: | ||||||||||||||||||||||||||||
| if offer.property_id.buyer_id: | ||||||||||||||||||||||||||||
| raise UserError( | ||||||||||||||||||||||||||||
| "a buyer is already assigned , therefore another offer has been accepted" | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| offer.state = "accepted" | ||||||||||||||||||||||||||||
| offer.property_id.selling_price = self.price | ||||||||||||||||||||||||||||
| offer.property_id.buyer_id = self.partner_id | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| return True | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| def action_refuse_offer(self): | ||||||||||||||||||||||||||||
| for offer in self: | ||||||||||||||||||||||||||||
| offer.state = "refused" | ||||||||||||||||||||||||||||
| return True | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| @api.model | ||||||||||||||||||||||||||||
| def create(self, vals_list): | ||||||||||||||||||||||||||||
| for vals in vals_list: | ||||||||||||||||||||||||||||
| higher_offer = self.search_count( | ||||||||||||||||||||||||||||
| [ | ||||||||||||||||||||||||||||
| ("property_id.id", "=", vals["property_id"]), | ||||||||||||||||||||||||||||
| ("price", ">", vals["price"]), | ||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||
| 1, | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| if higher_offer: | ||||||||||||||||||||||||||||
| raise UserError("Can't create an offer with a lower price") | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| self.env["estate.property"].browse( | ||||||||||||||||||||||||||||
| vals["property_id"] | ||||||||||||||||||||||||||||
| ).state = "offer_received" | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| return super().create(vals_list) | ||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| from odoo import fields, models | ||
|
|
||
|
|
||
| class EstatePropertyTag(models.Model): | ||
| _name = "estate.property.tag" | ||
| _description = "Property Tag" | ||
| _order = "name" | ||
|
|
||
| name = fields.Char(required=True) | ||
| color = fields.Integer() | ||
| _check_name_unique = models.Constraint( | ||
| "UNIQUE(name)", "A property tag name must be unique." | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| from odoo import api, fields, models | ||
|
|
||
|
|
||
| class EstatePropertyType(models.Model): | ||
| _name = "estate.property.type" | ||
| _description = "Property Type" | ||
| _order = "sequence" | ||
|
|
||
| name = fields.Char(required=True) | ||
| sequence = fields.Integer() | ||
| property_ids = fields.One2many("estate.property", "estate_property_type_id") | ||
| offer_ids = fields.One2many("estate.property.offer", "property_type_id") | ||
| offer_count = fields.Integer(compute="_compute_offer_count") | ||
|
|
||
| _check_name_unique = models.Constraint( | ||
| "UNIQUE(name)", "A property type name must be unique." | ||
| ) | ||
|
|
||
| @api.depends("offer_ids") | ||
| def _compute_offer_count(self): | ||
| for type in self: | ||
| type.offer_count = len(type.offer_ids) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| from odoo import fields, models | ||
|
|
||
|
|
||
| class ResUsers(models.Model): | ||
| _inherit = "res.users" | ||
|
|
||
| property_ids = fields.One2many( | ||
| "estate.property", | ||
| "salesperson_id", | ||
| domain=[("state", "not in", ["sold", "cancelled"])], | ||
| ) |
| 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_property_access_user,estate.property.user,model_estate_property,base.group_user,1,1,1,1 | ||
| estate_property_type_access_user,estate.property.type.user,model_estate_property_type,base.group_user,1,1,1,1 | ||
| estate_property_tag_access_user,estate.property.tag.user,model_estate_property_tag,base.group_user,1,1,1,1 | ||
| estate_property_offer_access_user,estate.property.offer.user,model_estate_property_offer,base.group_user,1,1,1,1 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| from . import test_estate_property |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| from odoo.exceptions import ValidationError | ||
| from odoo.tests import TransactionCase | ||
| from odoo import Command | ||
|
|
||
|
|
||
| class TestEstateProperty(TransactionCase): | ||
|
|
||
| @classmethod | ||
| def setUpClass(cls): | ||
| super().setUpClass() | ||
| cls.estate = cls.env['estate.property'].create({ | ||
| 'name': 'Super test estate', | ||
| 'expected_price': 100000.0, | ||
| 'state': 'new', | ||
| }) | ||
| cls.test_partner = cls.env['res.partner'].create({ | ||
| 'name': 'Maman ours', | ||
| }) | ||
|
|
||
| def test_estate_best_price(self): | ||
| ''' | ||
| Ensure best price is correctly updated when an offer is received. | ||
| ''' | ||
| self.assertEqual(self.estate.best_price, 0.0) | ||
| self.estate.offer_ids = [Command.create({ | ||
| 'price': 125000.0, | ||
| 'partner_id': self.test_partner.id, | ||
| })] | ||
| self.assertEqual(self.estate.best_price, 125000.0) | ||
|
|
||
| def test_accept_offer_south_facing_garden(self): | ||
| ''' | ||
| Ensure offers for estates with south-facing gardens can only be accepted if above expected | ||
| price. | ||
| ''' | ||
| self.estate.expected_price = 500000 | ||
| self.estate.offer_ids = [Command.create({ | ||
| 'price': 475000.0, | ||
| 'partner_id': self.test_partner.id, | ||
| })] | ||
| with self.assertRaises(ValidationError): | ||
| self.estate.offer_ids.accept_offer() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| <?xml version="1.0"?> | ||
| <odoo> | ||
| <menuitem id="estate_menu_root" name="Estate"> | ||
| <menuitem id="estate_menu_advertisement" name="Advertisement"> | ||
| <menuitem id="estate_property_menu" action="estate_property_action"/> | ||
| </menuitem> | ||
| <menuitem id="estate_settings_menu" name="Settings"> | ||
| <menuitem id="estate_property_type_menu_action" action="estate_property_type_action"/> | ||
| <menuitem id="estate_property_tag_menu_action" action="estate_property_tag_action"/> | ||
| </menuitem> | ||
|
Comment on lines
+7
to
+10
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. XMLid naming is not correct here |
||
| </menuitem> | ||
| </odoo> | ||
Uh oh!
There was an error while loading. Please reload this page.