-
Notifications
You must be signed in to change notification settings - Fork 2.8k
19.0 real estate tutorial jakan #1109
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
71296e3
3665d34
d8791e0
378ed7c
f155702
1c69bbe
9131c01
2784e60
8c66cd9
dbb059f
aee8aeb
6dd7eec
ac29768
54f03a6
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,22 @@ | ||
| { | ||
| 'name': "Real Estate", | ||
| 'version': "1.0", | ||
| 'category': "Real Estate", | ||
| 'summary': "Manage real estate properties", | ||
| 'description': "This module allows managing properties", | ||
| 'depends': ['base'], | ||
| 'author': "jakan", | ||
| 'license': "LGPL-3", | ||
| 'application': True, | ||
| 'installable': True, | ||
| '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_property_maintenance_views.xml", | ||
| 'views/res_users_views.xml', | ||
| "views/estate_menus.xml", | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| from . import estate_property | ||
| from . import estate_property_type | ||
| from . import estate_property_tag | ||
| from . import estate_property_offer | ||
| from . import estate_property_maintenance | ||
| from . import res_users |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| 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, float_is_zero | ||
|
|
||
|
|
||
| class EstateProperty(models.Model): | ||
| _name = 'estate.property' | ||
| _description = 'Real Estate Property' | ||
| _order = 'id desc' | ||
|
|
||
| # Each field becomes a column in PostgreSQL table | ||
| name = fields.Char(required=True, default="Unknown") | ||
| description = fields.Text() | ||
| postcode = fields.Char() | ||
| date_availability = fields.Date( | ||
| default=lambda self: fields.Date.today() + relativedelta(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() | ||
| garage = fields.Boolean() | ||
| garden = fields.Boolean() | ||
| garden_area = fields.Integer() | ||
| garden_orientation = fields.Selection( | ||
| string="Direction", | ||
| 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"), | ||
| ], | ||
| required=True, | ||
| copy=False, | ||
| default="new", | ||
| ) | ||
| property_type_id = fields.Many2one("estate.property.type", string="Property Type") | ||
| buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False) | ||
| seller_id = fields.Many2one("res.users", string="Seller", default=lambda self: self.env.user) | ||
| tag_ids = fields.Many2many("estate.property.tag", string="Property Tags") | ||
| offer_ids = fields.One2many("estate.property.offer", "property_id", string="Property Offers") | ||
| total_area = fields.Float(string="Total Area", compute="_compute_total_area", store=True) | ||
| best_price = fields.Float(string="Best Offer", compute="_compute_best_price") | ||
| maintenance_ids = fields.One2many("estate.property.maintenance", "property_id") | ||
| total_cost = fields.Float(string="Total Cost", compute="_compute_total_cost") | ||
|
|
||
| @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.depends('offer_ids.price') | ||
| def _compute_best_price(self): | ||
| for property in self: | ||
| if property.offer_ids: | ||
| property.best_price = max(property.offer_ids.mapped('price')) | ||
| else: | ||
| property.best_price = 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 | ||
|
|
||
| def action_sold(self): | ||
| for property in self: | ||
| if property.state == 'cancelled': | ||
| raise UserError("Sold property cannot be cancelled") | ||
|
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. How can you make this translatable? |
||
| if not property.buyer_id: | ||
| raise UserError("Without accept any offer we can't sold it") | ||
| if property.maintenance_ids: | ||
| for maintenance in property.maintenance_ids: | ||
| if maintenance.status in ('new', 'cancle'): | ||
| raise UserError("Maintenance cost must be Approved or Done") | ||
| property.state = 'sold' | ||
| return True | ||
|
|
||
| def action_cancel(self): | ||
| for property in self: | ||
| if property.state == 'sold': | ||
| raise UserError("Cancelled property cannot be sold") | ||
| property.state = 'cancelled' | ||
| return True | ||
|
|
||
| _check_expected_price_positive = models.Constraint( | ||
|
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. Follow this conventions while declaring fields, constraints, methods or CRUD operations. |
||
| 'CHECK(expected_price > 0)', | ||
| 'Expected price must be strictly positive.', | ||
| ) | ||
|
|
||
| _check_selling_price_positive = models.Constraint( | ||
| 'CHECK(selling_price > 0)', | ||
| 'Selling price must be positive.', | ||
| ) | ||
|
|
||
| @api.constrains('expected_price', 'selling_price') | ||
| def _check_selling_price(self): | ||
| for property in self: | ||
| if float_is_zero(property.selling_price, precision_digits=2): | ||
| continue | ||
|
|
||
| minimum_price = property.expected_price * 0.9 | ||
|
|
||
| if float_compare(property.selling_price, minimum_price, precision_digits=2) < 0: | ||
| raise ValidationError("Selling price cannot be lower than 90% of the expected price.") | ||
|
|
||
| @api.depends('maintenance_ids.cost') | ||
| def _compute_total_cost(self): | ||
| for maintenance in self: | ||
| maintenance.total_cost = sum(maintenance.maintenance_ids.mapped('cost')) | ||
|
|
||
| @api.ondelete(at_uninstall=False) | ||
| def _check_property_deletion(self): | ||
| for property in self: | ||
| if property.state not in ('new', 'cancelled'): | ||
| raise UserError("You can only delete properties in New or Cancelled state") | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| from odoo import fields, models | ||
| from odoo.exceptions import UserError | ||
| from odoo.tools.float_utils import float_is_zero | ||
|
|
||
|
|
||
| class EstatePropertyMaintenance(models.Model): | ||
| _name = 'estate.property.maintenance' | ||
| _description = 'Estate Property maintenance' | ||
|
|
||
| title = fields.Char(required=True) | ||
| cost = fields.Float(string="Cost") | ||
| status = fields.Selection( | ||
| [ | ||
| ('new', "New"), | ||
| ('approved', "Approved"), | ||
| ('done', "Done"), | ||
| ('cancle', "Cancle") | ||
| ], | ||
| required=True, | ||
| default="new", | ||
| ) | ||
|
|
||
| property_id = fields.Many2one('estate.property', required=True) | ||
|
|
||
| def maintenance_accept(self): | ||
| for maintenance in self: | ||
| if float_is_zero(maintenance.cost, precision_digits=2): | ||
| raise UserError("Maintenance cost must be greater than zero") | ||
|
|
||
| maintenance.status = "approved" | ||
| return True | ||
|
|
||
| def maintenance_refuse(self): | ||
| for maintenance in self: | ||
| maintenance.status = "cancle" | ||
| return True |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| from dateutil.relativedelta import relativedelta | ||
|
|
||
| from odoo import api, fields, models | ||
| from odoo.exceptions import UserError, ValidationError | ||
|
|
||
|
|
||
| class EstatePropertyOffer(models.Model): | ||
| _name = 'estate.property.offer' | ||
| _description = 'Estate Property Offer' | ||
| _order = 'price desc' | ||
|
|
||
| price = fields.Float(string="Price") | ||
| status = fields.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(default=7) | ||
| date_deadline = fields.Date( | ||
| compute="_compute_date_deadline", | ||
| inverse="_inverse_date_deadline", | ||
| store=True | ||
| ) | ||
| property_type_id = fields.Many2one("estate.property.type", related="property_id.property_type_id", store=True, readonly=True) | ||
|
|
||
| @api.depends("validity", "create_date") | ||
| def _compute_date_deadline(self): | ||
| for offer in self: | ||
| if offer.create_date: | ||
| offer.date_deadline = ( | ||
| offer.create_date.date() | ||
| + relativedelta(days=offer.validity) | ||
| ) | ||
| else: | ||
| offer.date_deadline = fields.Date.today() + relativedelta(days=offer.validity) | ||
|
|
||
| def _inverse_date_deadline(self): | ||
| for offer in self: | ||
| if offer.create_date and offer.date_deadline: | ||
| offer.validity = ( | ||
| offer.date_deadline | ||
| - offer.create_date.date() | ||
| ).days | ||
|
|
||
| def action_accept(self): | ||
| for offer in self: | ||
| if offer.property_id.buyer_id: | ||
| raise UserError("Property already accepted") | ||
|
|
||
| other_offer = offer.property_id.offer_ids - offer | ||
| other_offer.write({'status': 'refused'}) | ||
|
|
||
| offer.status = "accepted" | ||
| offer.property_id.buyer_id = offer.partner_id | ||
| offer.property_id.selling_price = offer.price | ||
| offer.property_id.state = "sold" | ||
| offer.property_id.active = False | ||
| return True | ||
|
|
||
| def action_refuse(self): | ||
| for offer in self: | ||
| offer.status = "refused" | ||
| return True | ||
|
|
||
| _check_offer_price_positive = models.Constraint( | ||
|
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. Follow this conventions while declaring fields, constraints, methods or CRUD operations. |
||
| 'CHECK(price > 0)', | ||
| 'Offer price must be positive.', | ||
| ) | ||
|
|
||
| @api.constrains('property_id') | ||
| def _check_property_state(self): | ||
| for offer in self: | ||
| if offer.property_id.state in ('sold', 'cancelled'): | ||
| raise ValidationError("You cannot add an offer on a Sold or Cancelled property") | ||
|
|
||
| @api.model | ||
| def create(self, vals_list): | ||
| for vals in vals_list: | ||
| property_id = vals.get("property_id") | ||
| price = vals.get("price") | ||
| property_rec = self.env["estate.property"].browse(property_id) | ||
|
|
||
| # Prevent lower offer | ||
| existing_prices = property_rec.offer_ids.mapped("price") | ||
| if existing_prices and price < max(existing_prices): | ||
| raise UserError("You cannot create an offer lower than an existing offer") | ||
|
|
||
| # Set property state | ||
| property_rec.state = "offer_received" | ||
|
|
||
| return super().create(vals_list) | ||
| 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(required=True) | ||
| color = fields.Integer() | ||
|
|
||
| _unique_property_tag_name = models.Constraint( | ||
| 'UNIQUE(name)', | ||
| 'Property tag name must be unique.', | ||
| ) |
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -0,0 +1,24 @@ | ||||
| from odoo import fields, models, api | ||||
|
|
||||
|
|
||||
| class EstatePropertyType(models.Model): | ||||
| _name = 'estate.property.type' | ||||
| _description = 'Estate Property Type' | ||||
| _order = 'sequence, name' | ||||
|
|
||||
| name = fields.Char(required=True) | ||||
| sequence = fields.Integer(default=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.
Suggested change
|
||||
| property_ids = fields.One2many("estate.property", "property_type_id", string="Properties") | ||||
| offer_ids = fields.One2many("estate.property.offer", "property_type_id") | ||||
| offer_count = fields.Integer(string="Offers", compute="_compute_offer_count") | ||||
|
|
||||
| _unique_property_type_name = models.Constraint( | ||||
| 'UNIQUE(name)', | ||||
| 'Property type name must be unique.', | ||||
| ) | ||||
|
|
||||
| @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,11 @@ | ||
| from odoo import fields, models | ||
|
|
||
|
|
||
| class ResUsers(models.Model): | ||
| _inherit = "res.users" | ||
|
|
||
| property_ids = fields.One2many( | ||
| 'estate.property', | ||
| 'seller_id', | ||
| string="Assigned Properties", | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| 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 | ||
| access_estate_property_maintenance,access_estate_property_maintenance,model_estate_property_maintenance,base.group_user,1,1,1,1 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <odoo> | ||
| <menuitem id="estate_menu_root" name="Real Estate"> | ||
| <menuitem id="estate_advertisements_menu" name="Advertisements"> | ||
| <menuitem id="estate_property_menu_action" name="Properties" action="estate_property_action"/> | ||
| </menuitem> | ||
| <menuitem id="estate_property_settings" name="Setting"> | ||
| <menuitem id="estate_property_type_menu_action" name="Property Types" action="estate_property_type_action"/> | ||
| <menuitem id="estate_property_tag_menu_action" name="Property Tags" action="estate_property_tag_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.
What's the need to use this lambda function?