-
Notifications
You must be signed in to change notification settings - Fork 3.1k
rohar - Technical Training #1242
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
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,2 @@ | ||
|
|
||
| from . import models |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| { | ||
| "name": "Estate", | ||
| "version": "19.0.1.0.0", | ||
| "depends": [ | ||
| "base", | ||
| ], | ||
| "author": "Roman (rohar)", | ||
| "description": """ | ||
| Real Estate Advertisement | ||
| """, | ||
| # data files always loaded at installation | ||
| "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/estate_menus.xml", | ||
| "views/res_user_views.xml", | ||
| ], | ||
| # data files containing optionally loaded demonstration data | ||
| "demo": [], | ||
| "application": True, | ||
| "license": "LGPL-3", | ||
| } |
| 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,113 @@ | ||
| from odoo import api, fields, models | ||
| from odoo.exceptions import UserError, ValidationError | ||
| from odoo.tools.float_utils import float_compare, float_is_zero | ||
|
|
||
|
|
||
| class EstateProperty(models.Model): | ||
| _name = "estate.property" | ||
| _description = "Real estate property" | ||
| _order = "id desc" | ||
|
|
||
| name = fields.Char("Property Name", required=True) | ||
| description = fields.Text() | ||
| postcode = fields.Char() | ||
| date_availability = fields.Date( | ||
| "Available From", | ||
| default=fields.Date.add( | ||
| fields.Date.today(), | ||
| months=3, | ||
| ), | ||
| copy=False, | ||
| ) | ||
| expected_price = fields.Float(required=True) | ||
| selling_price = fields.Float(readonly=True, copy=False) | ||
| bedrooms = fields.Integer(default=2) | ||
| living_area = fields.Integer() | ||
| facades = fields.Integer() | ||
| has_garage = fields.Boolean("Garage") | ||
| has_garden = fields.Boolean("Garden") | ||
| garden_area = fields.Integer() | ||
| garden_orientation = fields.Selection( | ||
| [ | ||
| ("north", "North"), | ||
| ("south", "South"), | ||
| ("east", "East"), | ||
| ("west", "West"), | ||
| ], | ||
| ) | ||
| active = fields.Boolean(default=True) | ||
| state = fields.Selection( | ||
| [ | ||
| ("new", "New"), | ||
| ("offer_received", "Offer Received"), | ||
| ("offer_accepted", "Offer Accepted"), | ||
| ("sold", "Sold"), | ||
| ("cancelled", "Cancelled"), | ||
| ], | ||
| default="new", | ||
| ) | ||
| buyer_id = fields.Many2one("res.partner", copy=False) | ||
| salesperson_id = fields.Many2one("res.users", default=lambda self: self.env.user) | ||
| type_id = fields.Many2one("estate.property.type", string="Property Type") | ||
| tag_ids = fields.Many2many("estate.property.tag") | ||
| 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") | ||
|
|
||
| @api.depends("garden_area", "living_area") | ||
| def _compute_total_area(self): | ||
| for record in self: | ||
| record.total_area = record.garden_area + record.living_area | ||
|
|
||
| @api.depends("offer_ids.price") | ||
| def _compute_best_price(self): | ||
| for record in self: | ||
| record.best_price = max(record.offer_ids.mapped("price"), default=0) | ||
|
|
||
| @api.onchange("has_garden") | ||
| def _onchange_has_garden(self): | ||
| if self.has_garden: | ||
| self.garden_orientation = "north" | ||
| self.garden_area = 10 | ||
| else: | ||
| self.garden_orientation = None | ||
| self.garden_area = 0 | ||
|
|
||
| def action_sold(self): | ||
| for record in self: | ||
| if record.state == "cancelled": | ||
| raise UserError("A cancelled property cannot be sold !") | ||
|
|
||
| record.state = "sold" | ||
| return True | ||
|
|
||
| def action_cancel(self): | ||
| for record in self: | ||
| if record.state == "sold": | ||
| raise UserError("A sold property cannot be cancelled !") | ||
|
|
||
| record.state = "cancelled" | ||
| return True | ||
|
|
||
| _check_expected_price = models.Constraint( | ||
| "CHECK(expected_price > 0)", | ||
| "A property expected price must be stricly positive", | ||
| ) | ||
|
|
||
| _check_selling_price = models.Constraint( | ||
| "CHECK(selling_price >= 0)", | ||
| "A property selling price must be positive", | ||
| ) | ||
|
|
||
| @api.constrains("selling_price", "expected_price") | ||
| def check_selling_price(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. should be named '_check_selling_price' |
||
| for record in self: | ||
| if float_is_zero(record.selling_price, precision_digits=2): | ||
| return | ||
|
Comment on lines
+105
to
+106
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 an exception for 0 ? |
||
| if float_compare(record.selling_price, 0.9 * record.expected_price, precision_digits=2) == -1: | ||
| raise ValidationError("The selling price cannot be lower than 90% of the expected price.") | ||
|
|
||
| @api.ondelete(at_uninstall=False) | ||
| def _unlink_if_new_or_cancelled(self): | ||
| if any(not record.state in ("new", "cancelled") for record in self): | ||
| raise UserError("Can only delete an new or cancelled property.") | ||
|
Comment on lines
+92
to
+113
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. Be careful about attribute ordering |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| from odoo import api, fields, models | ||
| from odoo.tools.float_utils import float_compare | ||
| from odoo.exceptions import UserError, ValidationError | ||
| from dateutil.relativedelta import relativedelta | ||
|
|
||
|
|
||
| class EstatePropertyOffer(models.Model): | ||
| _name = "estate.property.offer" | ||
| _description = "Property offer" | ||
| _order = "price desc" | ||
|
|
||
| price = fields.Float() | ||
| status = fields.Selection( | ||
| [("accepted", "Accepted"), ("refused", "Refused")], copy=False | ||
| ) | ||
|
rohar-odoo marked this conversation as resolved.
|
||
| partner_id = fields.Many2one("res.partner", required=True) | ||
| property_id = fields.Many2one("estate.property", required=True) | ||
| property_type_id = fields.Many2one(related="property_id.type_id", store=True) | ||
| validity = fields.Integer("Validity (days)", default=7) | ||
| date_deadline = fields.Date( | ||
| "Deadline", compute="_compute_date_deadline", inverse="_inverse_date_deadline" | ||
| ) | ||
|
|
||
| _check_price = models.Constraint( | ||
| "CHECK(price > 0)", | ||
| "An offer price must be stricly positive", | ||
| ) | ||
|
|
||
| @api.depends("validity", "create_date") | ||
| def _compute_date_deadline(self): | ||
| for record in self: | ||
| date = record.create_date if record.create_date else fields.Date.today() | ||
| record.date_deadline = date + relativedelta(days=record.validity) | ||
|
|
||
| @api.model | ||
| def create(self, vals_list): | ||
| property = self.env["estate.property"].browse(vals_list[0]["property_id"]) | ||
| if max(property.offer_ids.mapped("price"), default=0.0) > vals_list[0]["price"]: | ||
| raise UserError("New offer must be higher than existing ones.") | ||
|
|
||
| property.state = "offer_received" | ||
|
|
||
| return super().create(vals_list) | ||
|
Comment on lines
+35
to
+43
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. What if I create multiple records ? (multiple values in vals_list) then only the first one would be checked |
||
|
|
||
| def _inverse_date_deadline(self): | ||
| for record in self: | ||
| days = record.date_deadline - record.create_date.date() | ||
| record.validity = days.days | ||
|
|
||
| def action_accept(self): | ||
| for record in self: | ||
| property_id = record.property_id | ||
| if property_id.state in ("sold", "accepted"): | ||
| raise UserError("A sold or cancelled property can't be sold.") | ||
|
|
||
| if (property_id.has_garden and property_id.garden_orientation == "south" and float_compare(record.price, property_id.expected_price, precision_digits=2) == -1): | ||
| raise ValidationError("The selling price cannot be lower than 90% of the expected price.") | ||
|
Comment on lines
+56
to
+57
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. This should be in a check |
||
|
|
||
| record.status = "accepted" | ||
| property_id.state = "sold" | ||
| property_id.buyer_id = record.partner_id | ||
| property_id.selling_price = record.price | ||
|
|
||
| def action_refuse(self): | ||
| self.status = "refused" | ||
|
Comment on lines
+45
to
+65
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. Attribute ordering :) |
||
| 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 = "Property tag" | ||
| _order = "name" | ||
|
|
||
| name = fields.Char(required=True) | ||
| color = fields.Integer() | ||
|
|
||
| _check_name = models.Constraint( | ||
| "UNIQUE (name)", | ||
| "Each tag name must be unique", | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| from odoo import api, fields, models | ||
|
|
||
|
|
||
| class EstatePropertyType(models.Model): | ||
| _name = "estate.property.type" | ||
| _description = "Property type" | ||
| _order = "sequence desc, name" | ||
|
|
||
| name = fields.Char(required=True) | ||
| property_ids = fields.One2many("estate.property", "type_id", string="Properties") | ||
| offer_ids = fields.One2many("estate.property.offer", "property_type_id") | ||
| sequence = fields.Integer(default=1) | ||
| count = fields.Integer(compute="_compute_count") | ||
|
|
||
| _check_name = models.Constraint( | ||
| "UNIQUE (name)", | ||
| "Each type name must be unique", | ||
| ) | ||
|
|
||
| @api.depends("offer_ids") | ||
| def _compute_count(self): | ||
| for record in self: | ||
| record.count = len(record.offer_ids) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| from odoo import fields, models | ||
|
|
||
|
|
||
| class ResUsers(models.Model): | ||
| _name = "res.users" | ||
| _inherit = ["res.users"] | ||
|
|
||
| property_ids = fields.One2many( | ||
| "estate.property", | ||
| "salesperson_id", | ||
| domain=[("state", "in", ("new", "offer_received"))], | ||
| ) |
| 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_acces_user,estate.property.offer.user,model_estate_property_offer,base.group_user,1,1,1,1 | ||
|
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. typo |
||
| 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,44 @@ | ||
| 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.has_garden = True | ||
| self.estate.garden_orientation = 'south' | ||
| self.estate.offer_ids = [Command.create({ | ||
| 'price': 475000.0, | ||
| 'partner_id': self.test_partner.id, | ||
| })] | ||
| with self.assertRaises(ValidationError): | ||
| self.estate.offer_ids.action_accept() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| <?xml version="1.0"?> | ||
| <odoo> | ||
| <menuitem id="estate_property_root" name="Real Estate"> | ||
| <menuitem id="estate_property_menu_advertisement" name="Advertisement"> | ||
| <menuitem id="estate_property_menu" action="estate_property_action"/> | ||
| </menuitem> | ||
| <menuitem id="estate_property_menu_settings" name="Settings"> | ||
| <menuitem id="estate_property_type_menu" action="estate_property_type_action"/> | ||
| <menuitem id="estate_property_tag_menu" action="estate_property_tag_action"/> | ||
| </menuitem> | ||
| </menuitem> | ||
| </odoo> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| <?xml version="1.0"?> | ||
| <odoo> | ||
| <record id="estate_property_offer_view_list" model="ir.ui.view"> | ||
| <field name="name">estate.property.offer.list</field> | ||
| <field name="model">estate.property.offer</field> | ||
| <field name="arch" type="xml"> | ||
| <list string="Offers"> | ||
| <field name="price"/> | ||
| <field name="status"/> | ||
| <field name="partner_id"/> | ||
| <field name="property_id"/> | ||
| <field name="validity" optional="true"/> | ||
| <field name="date_deadline" optional="true"/> | ||
| <field name="property_type_id" optional="true"/> | ||
|
Comment on lines
+12
to
+14
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. Optional values are hide or show |
||
| </list> | ||
| </field> | ||
| </record> | ||
| <record id="estate_property_offer_action" model="ir.actions.act_window"> | ||
| <field name="name">Property Offers</field> | ||
| <field name="res_model">estate.property.offer</field> | ||
| <field name="domain">[('property_type_id', '=', active_id)]</field> | ||
| <field name="view_mode">list,form</field> | ||
| </record> | ||
| </odoo> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| <?xml version="1.0"?> | ||
| <odoo> | ||
| <record id="estate_property_tag_view_list" model="ir.ui.view"> | ||
| <field name="name">estate.property.tag.list</field> | ||
| <field name="model">estate.property.tag</field> | ||
| <field name="arch" type="xml"> | ||
| <list string="Tags" editable="bottom"> | ||
| <field name="name"/> | ||
| </list> | ||
| </field> | ||
| </record> | ||
| <record id="estate_property_tag_action" model="ir.actions.act_window"> | ||
| <field name="name">Property Tags</field> | ||
| <field name="res_model">estate.property.tag</field> | ||
| <field name="view_mode">list,form</field> | ||
| </record> | ||
| </odoo> |
Uh oh!
There was an error while loading. Please reload this page.