-
Notifications
You must be signed in to change notification settings - Fork 2.9k
luleg - Technical Training - Creation of the Real Estate Module #1176
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
9658532
64e5ae6
95a5568
63a9396
68e374f
adf087f
48de967
03a702a
4d76898
c731814
bb74433
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,35 @@ | ||
| { | ||
| 'name': "Real Estate", | ||
|
|
||
| 'summary': """ | ||
| Server framework 101: A New Application" | ||
| """, | ||
|
|
||
| 'description': """ | ||
| Starting module for "Server framework 101: A New Application" | ||
| """, | ||
|
|
||
| 'author': "Odoo", | ||
| 'website': "https://www.odoo.com/", | ||
| 'category': 'Tutorials', | ||
| 'version': '0.1', | ||
| 'application': True, | ||
| 'installable': True, | ||
| 'depends': ['base'], | ||
|
|
||
| 'data': [ | ||
| "data/estate.tag.csv", | ||
| "data/estate.property.type.csv", | ||
| "data/estate.property.csv", | ||
| "security/ir.model.access.csv", | ||
| "views/estate_property_views.xml", | ||
| "views/estate_property_offer_views.xml", | ||
| "views/estate_property_type_views.xml", | ||
| "views/estate_tag_views.xml", | ||
| "views/estate_menu_views.xml", | ||
| "views/res_user_views.xml" | ||
| ], | ||
| 'assets': { | ||
| }, | ||
| 'license': 'LGPL-3' | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| "id","name","expecting_price","living_area","total_area","property_type_id:id" | ||
| estate_tag_1,"Odoo Farm 2",300000,1900,2000,estate_property_type_1 | ||
| estate_tag_2,"Odoo LLN",1000000,0,40000,estate_property_type_3 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| "id","name","livable" | ||
| estate_property_type_1,House,1 | ||
| estate_property_type_2,Apartment,1 | ||
| estate_property_type_3,Office,0 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| "id","name","color" | ||
| estate_property_1,"Leased",5 | ||
| estate_property_2,"Empty",8 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| from . import estate_tag | ||
| from . import estate_property_type | ||
| from . import estate_property_offer | ||
| from . import estate_property | ||
| from . import res_users |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| # Part of Odoo. See LICENSE file for full copyright and licensing details. | ||
|
|
||
| from odoo import _, api, fields, models | ||
| from odoo.exceptions import UserError | ||
|
|
||
|
|
||
| class Property(models.Model): | ||
| _name = "estate.property" | ||
| _description = "Real Estate Property" | ||
| _order = "id" | ||
|
|
||
| name = fields.Char("Name", required=True, translate=True) | ||
|
|
||
| description = fields.Char("Description") | ||
|
|
||
| stage = fields.Selection([ | ||
| ("new", "New"), | ||
| ("offer_received", "Offer Received"), | ||
| ("offer_accepted", "Offer Accepted"), | ||
| ("sold", "Sold"), | ||
| ("cancelled", "Cancelled") | ||
| ], default="new", copy=False) | ||
|
|
||
| currency_id = fields.Many2one("res.currency", "Currency") | ||
| expecting_price = fields.Monetary("Expecting Price", required=True) | ||
| best_offer = fields.Monetary("Best Offer", default=0, compute="_compute_best_offer", store=True) | ||
| selling_price = fields.Monetary("Selling Price", default=0, readonly=True) | ||
|
|
||
| seller_id = fields.Many2one("res.users", string="Salesperson", index=True, default=lambda self: self.env.user) | ||
| buyer_id = fields.Many2one("res.partner", string="Buyer", index=True) | ||
|
|
||
| postcode = fields.Integer("Postcode") | ||
| bedroom_number = fields.Integer("Bedrooms", default=0) | ||
| facade_number = fields.Integer("Facades", default=0) | ||
| garage = fields.Boolean("Garage", default=False) | ||
| garden = fields.Boolean("Garden", default=False) | ||
|
|
||
| living_area = fields.Integer("Living Area (sqm)", default=0) | ||
| garden_area = fields.Integer("Garden Area (sqm)", default=0) | ||
| total_area = fields.Integer("Total Area (sqm)", default=0, compute="_compute_total_area", inverse="_inverse_total_area", store=True) | ||
|
|
||
| garden_orientation = fields.Selection([ | ||
| ("north", "North"), | ||
| ("south", "South"), | ||
| ("east", "East"), | ||
| ("west", "West") | ||
| ]) | ||
|
|
||
| def _current_date(self): | ||
| return fields.Date.today() | ||
|
|
||
| available_from = fields.Date("Date", default=_current_date) | ||
|
|
||
| active = fields.Boolean("Active", default=True) | ||
| sequence = fields.Integer(default=10) | ||
|
|
||
| property_type_id = fields.Many2one("estate.property.type", string="Property Type") | ||
| property_livable = fields.Boolean("Livable", compute="_compute_property_livable") | ||
|
|
||
| tag_ids = fields.Many2many("estate.tag", string="Tags") | ||
|
|
||
| offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers") | ||
|
|
||
| @api.depends("living_area", "garden_area") | ||
| def _compute_total_area(self): | ||
| for property in self: | ||
| property.total_area = property.living_area + property.garden_area | ||
|
|
||
| @api.onchange("total_area") | ||
| def _inverse_total_area(self): | ||
| for property in self: | ||
| temp_total_area = property.total_area | ||
| property.living_area = max(0, temp_total_area - property.garden_area) | ||
| property.garden_area = temp_total_area - property.living_area | ||
|
|
||
| @api.onchange("garden") | ||
| def _onchange_garden(self): | ||
| for property in self: | ||
| if property.garden: | ||
| property.garden_orientation = "north" | ||
| property.garden_area = 10 | ||
| else: | ||
| property.garden_orientation = "" | ||
| property.garden_area = 0 | ||
|
|
||
| @api.depends("property_type_id.livable") | ||
| def _compute_property_livable(self): | ||
| for property in self: | ||
| property.property_livable = property.property_type_id.livable | ||
|
|
||
| @api.depends("offer_ids.translated_price") | ||
| def _compute_best_offer(self): | ||
| for property in self: | ||
| property.best_offer = max([0, *property.offer_ids.mapped("translated_price")]) | ||
|
|
||
| def action_set_as_cancelled(self): | ||
| for property in self: | ||
| if property.stage in ["sold", "cancelled"]: | ||
| raise UserError(_("This property was already set as '%s'", Property.stage._selection[property.stage])) | ||
| property.stage = "cancelled" | ||
| return True | ||
|
|
||
| def action_set_as_sold(self): | ||
| for property in self: | ||
| if property.stage in ["sold", "cancelled"]: | ||
| raise UserError(_("This property was already set as '%s'", Property.stage._selection[property.stage])) | ||
| property.stage = "sold" | ||
| return True | ||
|
|
||
| @api.ondelete(at_uninstall=False) | ||
| def _unlink_except_if_advanced_stage(self): | ||
| for property in self: | ||
| if not property.stage in ["new", "cancelled"]: | ||
| raise UserError(_("You cannot delete this property (%s), it is not in a new or cancelled stage.", property.name)) | ||
|
|
||
| _check_bedroom_number = models.Constraint( | ||
| 'CHECK(bedroom_number >= 0)', | ||
| 'The number of bedrooms can\'t be negative.', | ||
| ) | ||
|
|
||
| _check_living_area = models.Constraint( | ||
| 'CHECK(living_area >= 0)', | ||
| 'The living area can\'t be negative.', | ||
| ) | ||
|
|
||
| _check_garden_area = models.Constraint( | ||
| 'CHECK(garden_area >= 0)', | ||
| 'The garden area can\'t be negative.', | ||
| ) | ||
|
|
||
| _check_total_area = models.Constraint( | ||
| 'CHECK(total_area >= 0)', | ||
| 'The total area can\'t be negative.', | ||
| ) | ||
|
|
||
| _check_expected_price = models.Constraint( | ||
| 'CHECK(expecting_price > 0)', | ||
| 'The expected price has to be stricly positive' | ||
| ) | ||
|
|
||
| _check_selling_price = models.Constraint( | ||
| 'CHECK(selling_price >= 0)', | ||
| 'The selling price has to be stricly positive' | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| from odoo import _, api, fields, models | ||
| from odoo.exceptions import UserError, ValidationError | ||
|
|
||
|
|
||
| class PropertyOffer(models.Model): | ||
| _name = "estate.property.offer" | ||
| _description = "Offer" | ||
| _order = "sequence, id" | ||
|
|
||
| property_id = fields.Many2one("estate.property", string="Property", required=True) | ||
| partner_id = fields.Many2one("res.partner", string="Partner", index=True, required=True) | ||
| property_type_id = fields.Many2one("estate.property.type", string="Property Type", related="property_id.property_type_id") | ||
|
|
||
| # Deadline Part | ||
|
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. It is a good practice to leave comments on your code. But consider cleaning them a bit. In this case, you have "deadline part", then a bit after you have "beginning of the deadline part" and then "end of the deadline part". In most of the cases, commenting only the beginning of a block is enough :) |
||
| def _current_date(self): | ||
| return fields.Date.today() | ||
|
|
||
| def _seven_days_from_now_date(self): | ||
| return fields.Date.add(fields.Date.today(), days=7) | ||
|
|
||
| deadline = fields.Date("Deadline", default=_seven_days_from_now_date) | ||
| creation_date = fields.Date("Creation Date", default=_current_date) | ||
| validity = fields.Integer("Validity (days)", store=True, compute="_compute_validity", inverse="_inverse_validity") | ||
|
|
||
| # Currency Part | ||
| currency_id = fields.Many2one("res.currency", "Currency") | ||
| property_currency_id = fields.Many2one("res.currency", "Partner Currency", related="property_id.currency_id") | ||
| price = fields.Monetary("Original Price", required=True) | ||
| translated_price = fields.Monetary("Price", store=True, compute="_compute_translated_price") | ||
|
|
||
| # State / validation part | ||
| status = fields.Selection([ | ||
| ("accepted", "Accepted"), | ||
| ("refused", "Refused"), | ||
| ], copy=False) | ||
|
|
||
| sequence = fields.Integer("Sequence", default=0) | ||
|
|
||
| # Deadline part | ||
| @api.depends("deadline") | ||
| def _compute_validity(self): | ||
| for offer in self: | ||
| offer.validity = (offer.deadline - offer.creation_date).days | ||
|
|
||
| # Reverse from _compute_validity, with real-time update because otherwise it's only after closing the form | ||
| @api.onchange("validity") | ||
| def _inverse_validity(self): | ||
| for offer in self: | ||
| offer.deadline = fields.Date.add(offer.creation_date, days=offer.validity) | ||
|
|
||
| # Currency part | ||
|
|
||
| # Translate currency to the one of the property so it's easier to compare | ||
| # Also, the webpage doesn't like showing multiple currency signs (as $ and €), | ||
| # so we put everything in the base currency for display | ||
|
|
||
| def _compute_currency(self): | ||
| if self.property_currency_id == self.currency_id: | ||
| return self.price | ||
| return self.currency_id._convert(self.price, self.property_currency_id) | ||
|
|
||
| @api.depends("property_currency_id", "price", "currency_id") | ||
| def _compute_translated_price(self): | ||
| for offer in self: | ||
| offer.translated_price = offer._compute_currency() | ||
|
|
||
| # Validation part | ||
| def action_confirm(self): | ||
| for offer in self: | ||
| if offer.property_id.stage in ["offer_accepted", "sold", "cancelled"]: | ||
| raise UserError(_("You can't accept new offers")) | ||
| offer.property_id.selling_price = offer.translated_price | ||
| offer.property_id.buyer_id = offer.partner_id | ||
| offer.property_id.stage = "offer_accepted" | ||
| offer.status = "accepted" | ||
|
|
||
| def action_refuse(self): | ||
| for offer in self: | ||
| offer.status = "refused" | ||
|
|
||
| @api.model_create_multi | ||
| def create(self, vals_list): | ||
| for val in vals_list: | ||
| property = self.env["estate.property"].browse(val["property_id"]) | ||
| if property.stage == "new": | ||
| property.stage = "offer_received" | ||
|
|
||
| if property.stage != "offer_received": | ||
| raise ValidationError(_("You can't create offers at this point")) | ||
|
|
||
| return super().create(vals_list) | ||
|
|
||
| _check_price = models.Constraint( | ||
| 'CHECK(price > 0)', | ||
| 'The price has to be stricly positive' | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| from odoo import api, fields, models | ||
|
|
||
|
|
||
| class PropertyType(models.Model): | ||
| _name = "estate.property.type" | ||
| _description = "Property Type" | ||
| _order = "name" | ||
|
|
||
| name = fields.Char("Name", required=True, translate=True) | ||
| livable = fields.Boolean("Livable", default=True) | ||
|
|
||
| property_ids = fields.One2many("estate.property", "property_type_id", string="Properties") | ||
| offer_ids = fields.One2many("estate.property.offer", "property_type_id", string="Property Offers") | ||
|
|
||
| offer_count = fields.Integer("Offer Count", compute="_compute_offer_count") | ||
|
|
||
| @api.depends("offer_ids") | ||
| def _compute_offer_count(self): | ||
| for property_type in self: | ||
| property_type.offer_count = len(property_type.offer_ids) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| # Part of Odoo. See LICENSE file for full copyright and licensing details. | ||
|
|
||
| from odoo import fields, models | ||
|
|
||
| from random import randint | ||
|
|
||
|
|
||
| class EstateTag(models.Model): | ||
| _name = "estate.tag" | ||
| _description = "Estate Tag" | ||
| _order = "name" | ||
|
|
||
| def _default_color(self): | ||
| return randint(1, 11) | ||
|
|
||
| name = fields.Char("Name", required=True, translate=True) | ||
| color = fields.Integer( | ||
| string='Color Index', default=lambda self: self._default_color(), | ||
| help='Tag color. No color means no display in kanban or front-end, to distinguish internal tags from public categorization tags.') | ||
|
|
||
| _unique_name = models.Constraint( | ||
| 'UNIQUE(name)', | ||
| 'The tag name has to be unique', | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| from odoo import fields, models | ||
|
|
||
| class ResUsers(models.Model): | ||
| _inherit = "res.users" | ||
|
|
||
| # Domain doesn't work | ||
| property_ids = fields.One2many("estate.property", "seller_id", string="Properties List", | ||
| domain="[('available_from', '<=', 'today'), ('stage', '!=', 'cancelled'), ('stage', '!=', 'sold')]") | ||
|
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. Domain doesn't work because it is supposed to be a list, not a string :D |
||
| 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,model_estate_property,base.group_user,1,1,1,1 | ||
| access_estate_property_type,access_estate_property_type,model_estate_property_type,base.group_user,1,1,1,1 | ||
| access_estate_property_offer,access_estate_property_offer,model_estate_property_offer,base.group_user,1,1,1,1 | ||
| access_estate_tag,access_estate_tag,model_estate_tag,base.group_user,1,1,1,1 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <odoo> | ||
| <menuitem id="estate_menu_root" | ||
| name="Real Estate" | ||
| web_icon="estate,static/description/icon.png" | ||
| sequence="99" | ||
| > | ||
| <menuitem id="estate_menu_properties" | ||
| name="Properties" | ||
| sequence="10" | ||
| > | ||
| <menuitem id="estate_menu_property" | ||
| name="Properties" | ||
| sequence="10" | ||
| action="estate.estate_property_action" | ||
| /> | ||
| <menuitem id="estate_menu_property_offer" | ||
| name="Offers" | ||
| sequence="20" | ||
| action="estate.estate_property_offer_action" | ||
| /> | ||
| </menuitem> | ||
| <menuitem id="estate_menu_config" | ||
| name="Configuration" | ||
| sequence="20" | ||
| > | ||
| <menuitem id="estate_menu_property_type" | ||
| name="Property Types" | ||
| sequence="20" | ||
| action="estate.estate_property_type_action" | ||
| /> | ||
| </menuitem> | ||
| </menuitem> | ||
| </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.
or
if property.state not in [....]