-
Notifications
You must be signed in to change notification settings - Fork 3.3k
aryep - Technical training #1389
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
4520620
55fd4a4
6636921
8bdc650
5ede1f3
7bf0a5b
27e083d
f8c5a59
9da69bc
c6d95e3
0f0d78a
f25c9d0
d76cac2
fe74bd8
1080e68
b9c0c74
17b56ef
e0c408a
a55c5a6
699843f
3420437
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,20 @@ | ||
| { | ||
| "name": "Estate", | ||
| "version": "1.0", | ||
| "summary": "Estate Management", | ||
| "depends": ["base"], | ||
| "application": True, | ||
| "installable": True, | ||
| "data": [ | ||
| 'security/ir.model.access.csv', | ||
|
|
||
| 'views/estate_property_views.xml', | ||
| 'views/estate_property_tag_views.xml', | ||
| 'views/estate_property_offer_views.xml', | ||
| 'views/estate_property_type_views.xml', | ||
| 'views/res_users_views.xml', | ||
| 'views/estate_menus.xml', | ||
| ], | ||
| "author": "Arturo Yepez", | ||
| "license": 'LGPL-3', | ||
| } |
|
aryep-odoo marked this conversation as resolved.
|
| 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_users |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| from dateutil.relativedelta import relativedelta | ||
|
|
||
| from odoo import _, api, fields, models | ||
| from odoo.exceptions import UserError, ValidationError | ||
| from odoo.tools.float_utils import float_compare | ||
|
|
||
|
|
||
| class EstateProperty(models.Model): | ||
| _name = "estate.property" | ||
| _description = "Estate Property Management" | ||
| _order = "id desc" | ||
|
|
||
| name = fields.Char(string="Title", required=True) | ||
| description = fields.Text() | ||
| postcode = fields.Char() | ||
| date_availability = fields.Date( | ||
| string="Available From", | ||
| default=lambda _: fields.Date.today() + relativedelta(months=3), | ||
| ) | ||
| expected_price = fields.Float(required=True) | ||
| selling_price = fields.Float(readonly=True, copy=False) | ||
| bedrooms = fields.Integer(default=2) | ||
| living_area = fields.Integer(string="Living Area (sqm)") | ||
| facades = fields.Integer() | ||
| garage = fields.Boolean() | ||
| garden = fields.Boolean() | ||
| garden_area = fields.Integer() | ||
| garden_orientation = fields.Selection( | ||
| string="Garden Orientation", | ||
| selection=[ | ||
| ("north", "North"), | ||
| ("south", "South"), | ||
| ("east", "East"), | ||
| ("west", "West"), | ||
| ], | ||
| ) | ||
| state = fields.Selection( | ||
| string="Status", | ||
| selection=[ | ||
| ("new", "New"), | ||
| ("offer_received", "Offer Received"), | ||
| ("offer_accepted", "Offer Accepted"), | ||
| ("sold", "Sold"), | ||
| ("cancelled", "Cancelled"), | ||
| ], | ||
| default="new", | ||
| copy=False, | ||
| ) | ||
| property_type_id = fields.Many2one( | ||
| string="Property Type", | ||
| comodel_name="estate.property.type", | ||
| ) | ||
| partner_id = fields.Many2one( | ||
| string="Buyer", | ||
| comodel_name="res.partner", | ||
| copy=False, | ||
| readonly=True, | ||
| ) | ||
| user_id = fields.Many2one( | ||
| string="Salesman", | ||
| comodel_name="res.users", | ||
| default=lambda self: self.env.user, | ||
| ) | ||
| tag_ids = fields.Many2many( | ||
| string="Property Tags", | ||
| comodel_name="estate.property.tag", | ||
| ) | ||
| offer_ids = fields.One2many( | ||
| string="Offers", | ||
| comodel_name="estate.property.offer", | ||
| inverse_name="property_id", | ||
| ) | ||
| total_area = fields.Integer( | ||
| string="Total Area (sqm)", | ||
| compute="_compute_total_area", | ||
| readonly=True, | ||
| ) | ||
| best_price = fields.Float( | ||
| string="Best Offer", | ||
| compute="_compute_best_price", | ||
| readonly=True, | ||
| ) | ||
| active = fields.Boolean(default=True) | ||
|
|
||
| _check_positive_expected_price = models.Constraint( | ||
| 'CHECK(expected_price > 0)', | ||
| 'Expected price must be a positive amount.', | ||
| ) | ||
| _check_positive_selling_price = models.Constraint( | ||
| 'CHECK(selling_price >= 0)', | ||
| 'Selling price must be a positive amount.', | ||
| ) | ||
|
|
||
| # Methods | ||
| @api.depends("living_area", "garden_area") | ||
| def _compute_total_area(self): | ||
| for record in self: | ||
| record.total_area = record.living_area + record.garden_area | ||
|
|
||
| @api.depends("offer_ids") | ||
| def _compute_best_price(self): | ||
| for record in self: | ||
| if record.offer_ids: | ||
| record.best_price = max(record.offer_ids.mapped("price")) | ||
| else: | ||
| record.best_price = 0.0 | ||
|
|
||
| @api.onchange("garden") | ||
| def _onchange_garden(self): | ||
| if self.garden: | ||
| self.garden_area = 10 | ||
| self.garden_orientation = "north" | ||
| else: | ||
| self.garden_area = 0 | ||
| self.garden_orientation = False | ||
|
|
||
| @api.constrains("selling_price") | ||
| def _check_selling_price(self): | ||
| for record in self: | ||
| if record.selling_price and float_compare(record.selling_price, 0.9 * record.expected_price, precision_digits=2) < 0: | ||
| raise ValidationError(_("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. Good job finding the translation function! It is a better practice to use it as self.env._(...) than simply _, as explained in its docstring, for the reasons explained in this commit. |
||
|
|
||
| @api.ondelete(at_uninstall=False) | ||
| def _unlink_if_not_new_or_cancelled(self): | ||
| for record in self: | ||
| if record.state not in ("new", "cancelled"): | ||
| raise ValidationError(_("Only new or cancelled properties can be deleted.")) | ||
|
|
||
| def action_sold(self): | ||
| self.ensure_one() | ||
|
|
||
| if not self.offer_ids: | ||
| raise ValidationError(_("Properties without offers can't be sold")) | ||
|
|
||
| if self.state == "cancelled": | ||
| raise UserError(_("Cancelled properties cannot be sold.")) | ||
|
|
||
| if self.state != "offer_accepted": | ||
| raise UserError(_("Only accepted offers can be sold.")) | ||
|
|
||
| self.state = "sold" | ||
|
|
||
| def action_cancel(self): | ||
| self.ensure_one() | ||
|
|
||
| if self.state == "sold": | ||
| raise UserError(_("Sold properties cannot be cancelled.")) | ||
|
|
||
| self.state = "cancelled" | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,86 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from dateutil.relativedelta import relativedelta | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from odoo import _, api, fields, models | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from odoo.exceptions import ValidationError | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class EstatePropertyOffer(models.Model): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _name = "estate.property.offer" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _description = "Estate Property Offer" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _order = "price desc" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| price = fields.Float(string="Offer Price", required=True) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| validity = fields.Integer(string="Validity (days)", default=7) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| date_deadline = fields.Date( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| string="Deadline", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| compute="_compute_date_deadline", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| inverse="_inverse_date_deadline", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| status = fields.Selection( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| string="Status", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| selection=[ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ("accepted", "Accepted"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ("refused", "Refused"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| copy=False, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| partner_id = fields.Many2one( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| string="Buyer", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| comodel_name="res.partner", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| required=True, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| property_id = fields.Many2one( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| string="Property", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| comodel_name="estate.property", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| required=True, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| property_type_id = fields.Many2one( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| string="Property Type", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| related="property_id.property_type_id", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| store=True, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _check_positive_price = models.Constraint( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'CHECK(price > 0)', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Offer prices must be a positive amount.', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Methods | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @api.depends("validity") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _compute_date_deadline(self): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for record in self: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| record.date_deadline = fields.Date.today() + relativedelta(days=record.validity) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _inverse_date_deadline(self): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for record in self: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| record.validity = (record.date_deadline - fields.Date.today()).days | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @api.model | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def create(self, vals_list): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for vals in vals_list: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| property = self.env["estate.property"].browse(vals.get("property_id")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if property and property.state in ("sold", "offer_accepted"): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise ValidationError(_("Can't create an offer for properties that are sold or with an accepted offer")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| property.state = "offer_received" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return super().create(vals_list) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def action_accept(self): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for record in self: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| record.status = "accepted" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| record.property_id.state = "offer_accepted" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| record.property_id.partner_id = record.partner_id | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| record.property_id.selling_price = record.price | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Refuse other offers for the same property | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| other_offers = self.search([ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ("property_id", "=", record.property_id.id), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ("id", "!=", record.id), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| other_offers.write({"status": "refused"}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+70
to
+82
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. Few things to say here:
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def action_refuse(self): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for record in self: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
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
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| record.status = "refused" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| from odoo import fields, models | ||
|
|
||
|
|
||
| class EstatePropertyTag(models.Model): | ||
| _name = "estate.property.tag" | ||
| _description = "Estate Property Tag" | ||
| _order = "name" | ||
|
|
||
| name = fields.Char(string="Tag Name", required=True) | ||
| color = fields.Integer(string="Color Index") | ||
|
|
||
| _unique_name = models.Constraint( | ||
| 'UNIQUE(name)', | ||
| 'Tag name must be unique.', | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| from odoo import fields, models | ||
|
|
||
|
|
||
| class EstatePropertyType(models.Model): | ||
| _name = "estate.property.type" | ||
| _description = "Estate Property Type" | ||
| _order = "sequence, name" | ||
|
|
||
| name = fields.Char(string="Property Type", required=True) | ||
| sequence = fields.Integer('Sequence', default=1, help="Used to order types. Lower is better.") | ||
| property_ids = fields.One2many( | ||
| string="Properties", | ||
| comodel_name="estate.property", | ||
| inverse_name="property_type_id", | ||
| ) | ||
| offer_ids = fields.One2many( | ||
| string="Offers", | ||
| comodel_name="estate.property.offer", | ||
| inverse_name="property_type_id", | ||
| ) | ||
| offer_count = fields.Integer( | ||
| string="Number of Offers", | ||
| compute="_compute_offer_count", | ||
| ) | ||
|
|
||
| # Methods | ||
| 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,11 @@ | ||
| from odoo import fields, models | ||
|
|
||
|
|
||
| class ResUsers(models.Model): | ||
| _inherit = "res.users" | ||
|
|
||
| property_ids = fields.One2many( | ||
| string="Real Estate Properties", | ||
| comodel_name="estate.property", | ||
| inverse_name="user_id", | ||
| ) |
| 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 | ||
| access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 | ||
| access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,1 | ||
| access_estate_property_tag,access_estate_property_tag,estate.model_estate_property_tag,base.group_user,1,1,1,1 | ||
| 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,4 @@ | ||
|
|
||
| from . import common | ||
| from . import test_estate_property | ||
| from . import test_estate_property_offer |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,27 @@ | ||||||
| from odoo.addons.base.tests.common import BaseCommon | ||||||
|
|
||||||
|
|
||||||
| class EstatePropertyCommon(BaseCommon): | ||||||
|
|
||||||
| @classmethod | ||||||
| def setUpClass(cls): | ||||||
| super().setUpClass() | ||||||
|
|
||||||
| cls.estate_property_types = cls.env["estate.property.type"].create([ | ||||||
| {"name": "House"}, | ||||||
| {"name": "Apartment"}, | ||||||
| ]) | ||||||
| cls.estate_property_tags = cls.env["estate.property.tag"].create([ | ||||||
| {"name": "tag1"}, | ||||||
| {"name": "tag2"}, | ||||||
| ]) | ||||||
|
Comment on lines
+10
to
+17
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 don't use them, do not create them 👌 |
||||||
| cls.estate_properties = cls.env["estate.property"].create([ | ||||||
|
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. Creating in batch is a great practice, but do you really need two properties? Their only difference is the price, which I don't think really changes anything in your tests. I would either:
As a side note, keeping the two properties together doesn't sound like the best idea here, since it forces you to call `search' at the start of each test, which is unnecessarily costly. Instead, you could do the following:
Suggested change
with more appropriate names if your properties have significant differences (e.g., |
||||||
| { | ||||||
| "name": "Property 1", | ||||||
| "expected_price": 100.0, | ||||||
| }, | ||||||
| { | ||||||
| "name": "Property 2", | ||||||
| "expected_price": 150.0, | ||||||
| }, | ||||||
| ]) | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| from odoo.tests import Form, tagged | ||
|
|
||
| from odoo.addons.estate.tests.common import EstatePropertyCommon | ||
|
|
||
|
|
||
| @tagged("post_install", "-at_install") | ||
| class EstatePropertyGardenCase(EstatePropertyCommon): | ||
|
|
||
| @classmethod | ||
| def setUpClass(cls): | ||
| super().setUpClass() | ||
|
|
||
| main_property = cls.estate_properties.search([("name", "=", "Property 1")]) | ||
| main_property.living_area = 90 | ||
| main_property.garden = True | ||
| main_property.garden_area = 100 | ||
| main_property.garden_orientation = "south" | ||
|
Comment on lines
+13
to
+17
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. You could just create a property with those values from the start, instead of searching and editing an existing one |
||
|
|
||
| def test_garden_deactivation(self): | ||
| """Test that when the Garden property is deactivated we get to see the change and deletion of previous values""" | ||
| main_property = self.estate_properties.search([("name", "=", "Property 1")]) | ||
| with Form(main_property) as property: | ||
| self.assertEqual(100, property.garden_area) | ||
| self.assertEqual("south", property.garden_orientation) | ||
| self.assertEqual(190, property.total_area) | ||
|
|
||
| property.garden = False | ||
|
|
||
| self.assertEqual(0, property.garden_area) | ||
| self.assertEqual(False, property.garden_orientation) | ||
| self.assertEqual(90, property.total_area) | ||
Uh oh!
There was an error while loading. Please reload this page.