-
Notifications
You must be signed in to change notification settings - Fork 3.3k
[ADD] estate: add initial module structure #1366
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
db2fc41
7440ded
e26b1c8
883ed90
ca2c41f
8660519
377c348
6dc496b
7b63c09
1030b23
a8fde85
fa0eab7
19b4dc8
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': 'Real Estate', | ||
| 'version': '1.0', | ||
| 'author': 'Odoo S.A.', | ||
| 'category': 'Real Estate/Brokerage', | ||
| 'summary': 'An estate module for training', | ||
| 'depends': ['base'], | ||
| '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_users_views.xml', | ||
| ], | ||
| 'installable': True, | ||
| '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_users |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| from odoo import api, fields, models | ||
| from odoo.exceptions import UserError | ||
| from odoo.tools import float_compare, float_is_zero | ||
|
|
||
|
|
||
| class EstateProperty(models.Model): | ||
| # Attributes | ||
| _name = 'estate.property' | ||
| _description = "Real Estate Property" | ||
| _order = 'id desc' | ||
|
|
||
| # Fields | ||
| name = fields.Char(string="Title", required=True) | ||
| description = fields.Text() | ||
| postcode = fields.Char() | ||
| date_availability = fields.Date( | ||
| string="Available From", | ||
| copy=False, | ||
| default=fields.Date.add(fields.Date.today(), 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)", default=0) | ||
| facades = fields.Integer() | ||
| garage = fields.Boolean() | ||
| garden = fields.Boolean() | ||
| garden_area = fields.Integer(string="Garden Area (sqm)", default=0) | ||
| garden_orientation = fields.Selection( | ||
| 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"), | ||
| ], | ||
| required=True, | ||
| copy=False, | ||
| default='new', | ||
| index=True, | ||
| ) | ||
| active = fields.Boolean(default=True) | ||
|
|
||
| # Relational Fields | ||
| property_type_id = fields.Many2one('estate.property.type', string="Property Type") | ||
| buyer_id = fields.Many2one('res.partner', string="Buyer", copy=False) | ||
| salesperson_id = fields.Many2one( | ||
| 'res.users', string="Salesman", default=lambda self: self.env.user | ||
| ) | ||
| tag_ids = fields.Many2many('estate.property.tag', string="Tags") | ||
| offer_ids = fields.One2many('estate.property.offer', 'property_id', string="Offers") | ||
|
|
||
| # Computed Fields | ||
| total_area = fields.Integer(string="Total Area (sqm)", compute='_compute_total_area') | ||
| best_price = fields.Float(string="Best Offer", compute='_compute_best_price', store=True) | ||
|
|
||
| # SQL Constraints | ||
| _check_expected_price = models.Constraint( | ||
| 'CHECK(expected_price > 0)', | ||
| "The expected price must be strictly positive.", | ||
| ) | ||
| _check_selling_price = models.Constraint( | ||
| 'CHECK(selling_price >= 0)', | ||
| "The selling price must be positive.", | ||
| ) | ||
|
|
||
| # Compute / Inverse 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.price') | ||
| def _compute_best_price(self): | ||
| for record in self: | ||
| # offer_ids is ordered by 'price desc', so index 0 gives the highest current offer | ||
| record.best_price = record.offer_ids[0].price if record.offer_ids else 0.0 | ||
|
SaddemAmine marked this conversation as resolved.
|
||
|
|
||
| # Onchange Methods | ||
| @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 | ||
|
|
||
| # Constrains Methods | ||
| @api.constrains('selling_price', 'expected_price') | ||
| def _check_selling_price_ratio(self): | ||
| for record in self: | ||
| if not float_is_zero(record.selling_price, precision_rounding=0.01): | ||
| if float_compare(record.selling_price, record.expected_price * 0.9, precision_rounding=0.01) < 0: | ||
|
SaddemAmine marked this conversation as resolved.
|
||
| raise UserError(self.env._("The selling price cannot be lower than 90% of the expected price!")) | ||
|
|
||
| # Helper Methods | ||
| def _check_new_offer_price(self, price): | ||
| self.ensure_one() | ||
| # offer_ids is ordered by 'price desc', so index 0 gives the highest current offer | ||
| if self.offer_ids and price <= self.offer_ids[0].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.
|
||
| raise UserError(self.env._("The offer amount must be strictly higher than existing offers.")) | ||
|
|
||
| def _accept_offer(self, buyer, price): | ||
| self.ensure_one() | ||
| if self.buyer_id: | ||
| raise UserError(self.env._("An offer has already been accepted for this property.")) | ||
| self.write({ | ||
| 'buyer_id': buyer.id, | ||
| 'selling_price': price, | ||
| 'state': 'offer_accepted', | ||
| }) | ||
|
|
||
| # CRUD Methods | ||
| @api.ondelete(at_uninstall=False) | ||
| def _unlink_except_new_or_cancelled(self): | ||
| for record in self: | ||
| if record.state not in {'new', 'cancelled'}: | ||
| raise UserError(self.env._("Only new and cancelled properties can be deleted.")) | ||
|
|
||
| # Action Methods | ||
| def action_cancel(self): | ||
| sold_properties = self.filtered(lambda record: record.state == 'sold') | ||
| if sold_properties: | ||
| names = ", ".join(sold_properties.mapped('name')) | ||
| raise UserError(self.env._("The following sold properties cannot be cancelled: %s.", names)) | ||
| self.write({'state': 'cancelled'}) | ||
| return True | ||
|
SaddemAmine marked this conversation as resolved.
|
||
|
|
||
| def action_sold(self): | ||
| cancelled_properties = self.filtered(lambda record: record.state == 'cancelled') | ||
| if cancelled_properties: | ||
| names = ", ".join(cancelled_properties.mapped('name')) | ||
| raise UserError(self.env._("The following cancelled properties cannot be sold: %s", names)) | ||
| not_accepted_properties = self.filtered(lambda prop: prop.state != 'offer_accepted') | ||
| if not_accepted_properties: | ||
| names = ", ".join(not_accepted_properties.mapped('name')) | ||
| raise UserError(self.env._("The following properties must have an accepted offer before being sold: %s", names)) | ||
| self.write({'state': 'sold'}) | ||
| return True | ||
|
Comment on lines
+139
to
+149
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. Keep in mind that public methods can be called on multiple records. You either have to block multi-recordsets using |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| from datetime import timedelta | ||
|
|
||
| from odoo import api, fields, models | ||
| from odoo.exceptions import UserError | ||
|
|
||
|
|
||
| class PropertyOffer(models.Model): | ||
| # Attributes | ||
| _name = 'estate.property.offer' | ||
| _description = "Real Estate Property Offer" | ||
| _order = 'price desc' | ||
|
|
||
| # Fields | ||
|
Comment on lines
+8
to
+13
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 here about the comments |
||
| price = fields.Float(string="Price") | ||
| status = fields.Selection( | ||
| selection=[ | ||
| ('accepted', "Accepted"), | ||
| ('refused', "Refused"), | ||
| ], | ||
| string="Status", | ||
| copy=False, | ||
| ) | ||
| partner_id = fields.Many2one('res.partner', string="Partner", required=True) | ||
| property_id = fields.Many2one('estate.property', string="Property", required=True) | ||
| validity = fields.Integer(string="Validity (days)", default=7) | ||
|
|
||
| # Relational Fields | ||
| property_type_id = fields.Many2one( | ||
| 'estate.property.type', | ||
| related='property_id.property_type_id', | ||
| string="Property Type", | ||
| store=True, | ||
| ) | ||
|
|
||
| # Computed Fields | ||
| date_deadline = fields.Date( | ||
| string="Deadline", | ||
| compute='_compute_date_deadline', | ||
| inverse='_inverse_date_deadline', | ||
| ) | ||
|
|
||
| # SQL Constraints | ||
| _check_price = models.Constraint( | ||
| 'CHECK(price > 0)', | ||
| "The offer price must be strictly positive.", | ||
| ) | ||
|
|
||
| # Compute / Inverse Methods | ||
| @api.depends('validity') | ||
| def _compute_date_deadline(self): | ||
| for record in self: | ||
| date = record.create_date.date() if record.create_date else fields.Date.today() | ||
| record.date_deadline = date + timedelta(days=record.validity) | ||
|
|
||
| def _inverse_date_deadline(self): | ||
| for record in self: | ||
| date = record.create_date.date() if record.create_date else fields.Date.today() | ||
| if record.date_deadline: | ||
| record.validity = (record.date_deadline - date).days | ||
|
|
||
| # CRUD Methods | ||
| @api.model_create_multi | ||
| def create(self, vals_list): | ||
| for vals in vals_list: | ||
| record = self.env['estate.property'].browse(vals['property_id']) | ||
| record._check_new_offer_price(vals['price']) | ||
| record.state = 'offer_received' | ||
| return super().create(vals_list) | ||
|
|
||
| # Action Methods | ||
| def action_accept(self): | ||
| if self.filtered(lambda offer: offer.status in {'accepted', 'refused'}): | ||
| raise UserError(self.env._("You can only accept pending offers.")) | ||
| for record in self: | ||
| record.property_id._accept_offer(record.partner_id, record.price) | ||
| other_offers = record.property_id.offer_ids - record | ||
| other_offers.write({'status': 'refused'}) | ||
| self.write({'status': 'accepted'}) | ||
| return True | ||
|
|
||
| def action_refuse(self): | ||
| if self.filtered(lambda offer: offer.status == 'accepted'): | ||
| raise UserError(self.env._("An accepted offer cannot be refused.")) | ||
| self.write({'status': 'refused'}) | ||
| return True | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| from odoo import fields, models | ||
|
|
||
|
|
||
| class PropertyTag(models.Model): | ||
| # Attributes | ||
| _name = 'estate.property.tag' | ||
| _description = "Real Estate Property Tag" | ||
| _order = 'name' | ||
|
|
||
| # Fields | ||
| name = fields.Char(required=True) | ||
| color = fields.Integer(string="Color") | ||
|
|
||
| # SQL Constraints | ||
| _unique_name = models.Constraint( | ||
| 'UNIQUE(name)', | ||
| "The property tag name must be unique.", | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| from odoo import api, fields, models | ||
|
|
||
|
|
||
| class PropertyType(models.Model): | ||
| # Attributes | ||
| _name = 'estate.property.type' | ||
| _description = "Real Estate Property Type" | ||
| _order = 'sequence, name' | ||
|
|
||
| # Fields | ||
| name = fields.Char(string="Name", required=True) | ||
| sequence = fields.Integer(string="Sequence") | ||
|
|
||
| # Relational Fields | ||
| property_ids = fields.One2many('estate.property', 'property_type_id', string="Properties") | ||
| offer_ids = fields.One2many('estate.property.offer', 'property_type_id', string="Offers") | ||
|
|
||
| # Computed Fields | ||
| offer_count = fields.Integer(string="Offers Count", compute='_compute_offer_count') | ||
|
|
||
| # SQL Constraints | ||
| _unique_name = models.Constraint( | ||
| 'UNIQUE(name)', | ||
| "The property type name must be unique.", | ||
| ) | ||
|
|
||
| # Compute Methods | ||
| @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,12 @@ | ||
| from odoo import fields, models | ||
|
|
||
|
|
||
| class ResUsers(models.Model): | ||
| _inherit = "res.users" | ||
|
|
||
| property_ids = fields.One2many( | ||
| "estate.property", | ||
| "salesperson_id", | ||
| string="Real Estate Properties", | ||
| 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 | ||
| 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_tag,access_estate_property_tag,model_estate_property_tag,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 |
|
SaddemAmine marked this conversation as resolved.
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <odoo> | ||
| <!-- Root Menu --> | ||
| <menuitem id="estate_menu_root" name="Real Estate"/> | ||
|
|
||
| <!-- Submenus --> | ||
| <menuitem id="estate_menu_advertisements" name="Advertisements" parent="estate_menu_root"/> | ||
| <menuitem id="estate_menu_settings" name="Settings" parent="estate_menu_root"/> | ||
|
|
||
| <!-- Action Menus --> | ||
| <menuitem id="estate_property_menu_action" | ||
| name="Properties" | ||
| parent="estate_menu_advertisements" | ||
| action="estate_property_action"/> | ||
| <menuitem id="estate_property_type_menu_action" | ||
| name="Property Types" | ||
| parent="estate_menu_settings" | ||
| action="estate_property_type_action"/> | ||
| <menuitem id="estate_property_tag_menu_action" | ||
| name="Property Tags" | ||
| parent="estate_menu_settings" | ||
| action="estate_property_tag_action"/> | ||
| </odoo> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <odoo> | ||
| <!-- Actions --> | ||
| <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="view_mode">list,form</field> | ||
| </record> | ||
|
|
||
| <!-- List View --> | ||
| <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="Property Offers" | ||
| editable="bottom" | ||
| decoration-success="status == 'accepted'" | ||
| decoration-danger="status == 'refused'"> | ||
| <field name="price"/> | ||
| <field name="partner_id"/> | ||
| <field name="validity"/> | ||
| <field name="date_deadline"/> | ||
| </list> | ||
| </field> | ||
| </record> | ||
| </odoo> |
Uh oh!
There was an error while loading. Please reload this page.