-
Notifications
You must be signed in to change notification settings - Fork 3.1k
[ADD] estate: Added the new real estate module #1228
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
Draft
ripil-odoo
wants to merge
30
commits into
odoo:19.0
Choose a base branch
from
odoo-dev:19.0-tutorial-training-ripil
base: 19.0
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+753
−0
Draft
Changes from all commits
Commits
Show all changes
30 commits
Select commit
Hold shift + click to select a range
eff5466
[ADD] estate: Added the new real estate module
ripil-odoo d51a356
[CLN] estate: Code cleanup in the new real estate module
ripil-odoo 365414a
[ADD] estate: Defined the estate properties model with basic fields
ripil-odoo cbdd03e
[ADD] estate: Setting Access Rights for the estate module
ripil-odoo 29b692e
[CLN] estate: Code cleanup in real estate module
ripil-odoo 390038b
[ADD] estate: Added menu and an action in estate module
ripil-odoo 1613039
[ADD] estate: Availability Date Calculation in estate module
ripil-odoo 13bb5d4
[ADD] estate: Custom list view in estate module (partial)
ripil-odoo 9b9b1a4
[ADD] estate: Custom list view in estate module
ripil-odoo 105b6e6
[ADD] estate: Custom field for form view in estate module (partial)
ripil-odoo be1dc76
[IMP] estate: Custom form view and search view for estate module
ripil-odoo 6c70217
[CLN] estate: Code format cleanup in estate module
ripil-odoo 0fb818e
[IMP] estate: Custom search view with filter and group by in estate m…
ripil-odoo c849d1c
[ADD] estate: Created new property type model in estate module
ripil-odoo af9d554
[FIX] estate: Missing view in manifest in estate module
ripil-odoo 82c6469
[ADD] estate: New Property Type and Property Tag models in real estat…
ripil-odoo 8d18d21
[CLN] estate: Code Cleanup in estate_properties.py
ripil-odoo e7ff64c
[ADD] estate: New Property Offer model in estate module
ripil-odoo 69fac9f
[IMP] estate: Computed fields in estate module
ripil-odoo 9172235
[IMP] estate: Method decorators in estate module
ripil-odoo 076b2df
[IMP] estate: Button actions in estate module
ripil-odoo ee96fad
[IMP] estate: UI decorations in estate module
ripil-odoo 62bb00b
[IMP] estate: Constraints in estate module and code review #1
ripil-odoo 6fcabca
[IMP] estate: SQL and python constraints in estate module
ripil-odoo 88248e5
[IMP] estate: widget, _order, and invisible in estate module
ripil-odoo 5c0578a
[IMP] estate: UI mods in estate module
ripil-odoo a757667
[IMP] estate: Stat button in estate module
ripil-odoo 48c7de9
[IMP] estate: Stat Button in estate module finished
ripil-odoo ade3985
[IMP] estate: Experimenting with stat buttons in estate module
ripil-odoo 8068552
[FIX] estate: Action not found error in estate module
ripil-odoo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| from . import models |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| { | ||
| 'name': 'Estate', | ||
| 'category': 'Real Estate', | ||
| 'sequence': 1, | ||
| 'summary': 'Making the real estate app from tutorials', | ||
| 'website': 'www.odoo.com', | ||
| 'depends': [ | ||
| 'base', | ||
| ], | ||
| 'data': [ | ||
| 'security/ir.model.access.csv', | ||
| 'views/estate_properties_view.xml', | ||
| 'views/estate_property_offer_view.xml', | ||
| 'views/estate_property_type_view.xml', | ||
| 'views/estate_property_tag_view.xml', | ||
| 'views/estate_menus.xml', | ||
| ], | ||
| 'installable': True, | ||
| 'application': True, | ||
| 'author': 'Rini Pillai', | ||
| 'license': 'LGPL-3', | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| from . import estate_properties | ||
| from . import estate_property_type | ||
| from . import estate_property_tag | ||
| from . import estate_property_offer |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,211 @@ | ||
| import logging | ||
| from dateutil import relativedelta as rd | ||
|
|
||
| from odoo import fields, models, api | ||
| from odoo.exceptions import UserError, ValidationError | ||
|
|
||
|
|
||
| _logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| def _get_availability_date(self): | ||
| # _logger.info("!!! %s", self) | ||
| return fields.Date.today() + rd.relativedelta(months=3) | ||
|
|
||
|
|
||
| def _get_salesperson(self): | ||
| # _logger.info(self.env.user.name) | ||
| # _logger.info(self.env.user.id) | ||
| # _logger.info(self.env.user) | ||
| return self.env.user | ||
|
|
||
|
|
||
| class EstateProperties(models.Model): | ||
| _name = 'estate.properties' | ||
| _description = 'Real Estate Properties' | ||
| _order = 'sequence' | ||
|
|
||
| active = fields.Boolean(help="Should the property be listed?", default=True) | ||
| bedrooms = fields.Integer(default=2) | ||
| best_price = fields.Float(compute='_compute_best_price') | ||
| buyer_id = fields.Many2one(comodel_name='res.partner', copy=False) | ||
| commission = fields.Integer(compute='_compute_commission', store=True) | ||
| date_availability = fields.Date(string="Availability Date", copy=False, default=lambda self: fields.Date.add(fields.Date.context_today(self), months=3)) | ||
| description = fields.Text() | ||
| expected_price = fields.Float(string="Expected Price", required=True) | ||
| facades = fields.Integer() | ||
| garage = fields.Boolean(string="Has Garage?", help="Does the proeprty have a garage?") | ||
| garden = fields.Boolean(string="Has Garden?", help="Does the property have a garden?") | ||
| garden_area = fields.Integer() | ||
| garden_orientation = fields.Selection( | ||
| [ | ||
| ('north', "North"), | ||
| ('south', "South"), | ||
| ('east', "East"), | ||
| ('west', "West") | ||
| ], | ||
| help="Directional orientation of the garden of the property shown" | ||
| ) | ||
| living_area = fields.Integer() | ||
| name = fields.Char(string="Property Name", required=True) | ||
| offer_ids = fields.One2many(comodel_name='estate.property.offer', inverse_name='property_id') | ||
| postcode = fields.Char() | ||
| price_gap = fields.Float(compute='_compute_price_gap') | ||
| property_type_colour = fields.Selection(related='property_type_id.colour', readonly=False) | ||
| property_type_id = fields.Many2one(comodel_name='estate.property.type') | ||
| salesperson_id = fields.Many2one(comodel_name='res.partner', default=lambda self: self.env.user.partner_id) | ||
| # salesperson = fields.Char(default=_get_salesperson) | ||
| selling_price = fields.Float(readonly=True, copy=False) | ||
| sequence = fields.Integer() | ||
| state = fields.Selection( | ||
| [ | ||
| ('new', "New"), | ||
| ('offer_received', "Offer Received"), | ||
| ('offer_accepted', "Offer Accepted"), | ||
| ('sold', "Sold"), | ||
| ('cancelled', "Cancelled") | ||
| ], | ||
| required=True, default='new', copy=False, string="Status" | ||
| ) | ||
| tag_ids = fields.Many2many(comodel_name='estate.property.tag') | ||
| total_area = fields.Integer(compute="_compute_total_area") | ||
|
|
||
| _check_expected_price = models.Constraint( | ||
| 'CHECK (expected_price > 0 AND selling_price >= 0)', | ||
| "Price should strictly be positive", | ||
| ) | ||
|
|
||
| @api.constrains('selling_price') | ||
| def _check_selling_price(self): | ||
| # breakpoint() | ||
| # _logger.error(self.id) | ||
| for property in self: | ||
| sp_threshold = 0.9 * property.expected_price | ||
| # _logger.error(sp_threshold) | ||
| # _logger.error(property.selling_price) | ||
| # _logger.error(property.expected_price) | ||
| # _logger.error(property.id) | ||
| if property.selling_price < sp_threshold: | ||
| raise ValidationError("Selling price cannot be less than 90% of the expected price") | ||
|
|
||
| @api.depends('living_area', 'garden_area') | ||
| def _compute_total_area(self): | ||
| # _logger.error(self) | ||
| for property in self: | ||
| # _logger.error(property._fields) | ||
| # _logger.error(property) | ||
| property.total_area = property.living_area + property.garden_area | ||
|
|
||
| @api.depends('offer_ids.price') | ||
| def _compute_best_price(self): | ||
| for property in self: | ||
| # _logger.error(self.mapped('offer_ids.price')) | ||
| offer_prices = property.mapped('offer_ids.price') | ||
| # _logger.error(offer_prices) | ||
| # _logger.error(max(offer_prices)) | ||
| property.best_price = max(offer_prices) if offer_prices else 0 | ||
|
|
||
| # @api.constrains('expected_price') | ||
| # def _check_expected_price(self): | ||
| # for property in self: | ||
| # if property.expected_price < 0: | ||
| # raise UserError("Value cannot be less than zero.") | ||
|
|
||
| @api.onchange('garden') | ||
| def _onchange_garden(self): | ||
| # _logger.error(self.garden) | ||
| if self.garden: | ||
| self.garden_area = 10 | ||
| self.garden_orientation = 'north' | ||
| else: | ||
| self.garden_area = 0 | ||
| self.garden_orientation = None | ||
|
|
||
| # @api.constrains('date_availability') | ||
| # def _check_date_availability(self): | ||
| # for property in self: | ||
| # if property.date_availability < fields.Date.context_today(property): | ||
| # raise ValidationError("Past dates not allowed") | ||
|
|
||
| @api.depends('best_price', 'expected_price') | ||
| def _compute_price_gap(self): | ||
| for property in self: | ||
| if property.best_price and property.expected_price: | ||
| property.price_gap = property.best_price - property.expected_price | ||
| else: | ||
| property.price_gap = 0 | ||
|
|
||
| @api.onchange('state') | ||
| def _onchnage_state(self): | ||
| if self.state == 'cancelled': | ||
| self.active = False | ||
| else: | ||
| self.active = True | ||
|
|
||
| def property_cancelled(self): | ||
| for property in self: | ||
| if property.state == 'cancelled': | ||
| raise UserError("Property already cancelled") | ||
| elif property.state != 'sold': | ||
| property.state = 'cancelled' | ||
| property.active = False | ||
| else: | ||
| raise UserError("A sold property cannot be cancelled") | ||
| return True | ||
|
|
||
| def property_sold(self): | ||
| for property in self: | ||
| if property.state == 'sold': | ||
| raise UserError("Property already sold") | ||
| elif property.state != 'cancelled': | ||
| if property.buyer_id: | ||
| property.state = 'sold' | ||
| else: | ||
| raise UserError("No buyer for this property yet") | ||
| else: | ||
| raise UserError("A cancelled property cannot be sold") | ||
| return True | ||
|
|
||
| @api.depends('selling_price') | ||
| def _compute_commission(self): | ||
| for property in self: | ||
| if property.selling_price: | ||
| property.commission = property.selling_price * 0.06 | ||
|
|
||
| def property_accept(self): | ||
| # breakpoint() | ||
| # best_offers = [] | ||
| # for property in self.offer_ids: | ||
| # best_offers.append(property.price) | ||
| # _logger.error(best_offers) | ||
| # property_to_accept = max(best_offers) | ||
| # _logger.error(property_to_accept) | ||
| # if self.offer_ids: | ||
| # best = self.offer_ids.search([('price', '=', self.best_price), ('status', 'not in', ['refused'])]) | ||
| # if best: | ||
| # best.offer_accepted() | ||
| # return | ||
| # best = self.offer_ids.search([('price', '<', self.best_price), ('status', 'not in', ['refused'])], 0, 1, order='price DESC') | ||
| # best.ensure_one() | ||
| # best.offer_accepted() | ||
| # # _logger.error(best) | ||
| # else: | ||
| # raise ValidationError("No offers listed for this property") | ||
| # for property in self.offer_ids: | ||
| # if property.status == 'accepted': | ||
| # pass | ||
| # else: | ||
| # property.status = 'refused' | ||
| self.ensure_one() | ||
| best_offer = self.offer_ids.filtered(lambda offer: offer.status not in ['accepted', 'refused']) | ||
|
|
||
| if not best_offer: | ||
| raise ValidationError("No offers listed for this property") | ||
|
|
||
| best_offer = best_offer.sorted(lambda offer: offer.price, reverse=True)[0] | ||
| return best_offer.offer_accepted() | ||
|
|
||
| @api.onchange('offer_ids') | ||
| def offer_received_state(self): | ||
| if self.offer_ids and self.state == 'new': | ||
| self.state = 'offer_received' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| import datetime | ||
| import logging | ||
|
|
||
| from odoo import api, fields, models | ||
| from odoo.exceptions import UserError | ||
| from odoo.orm.utils import ValidationError | ||
|
|
||
|
|
||
| _logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class EstatePropertyOffer(models.Model): | ||
| _name = 'estate.property.offer' | ||
| _description = 'Real Estate Property Offers' | ||
| _order = 'price desc' | ||
|
|
||
| deadline = fields.Date() | ||
| partner_id = fields.Many2one(comodel_name='res.partner', required=True) | ||
| price = fields.Float() | ||
| property_id = fields.Many2one(comodel_name='estate.properties', readonly=True) | ||
| property_type = fields.Char(related='property_id.property_type_id.type') | ||
| property_type_id = fields.Many2one(related='property_id.property_type_id') | ||
| status = fields.Selection( | ||
| [ | ||
| ('refused', "Refused"), | ||
| ('accepted', "Accepted") | ||
| ], | ||
| copy=False | ||
| ) | ||
| validity = fields.Integer(compute="_compute_validity", inverse="_inverse_deadline", default=7) | ||
|
|
||
| _check_offer_price = models.Constraint( | ||
| 'CHECK (price > 0)', | ||
| "Offer price should be positive" | ||
| ) | ||
|
|
||
| @api.depends('deadline') | ||
| def _compute_validity(self): | ||
| for offer in self: | ||
| offer.validity = (offer.deadline - offer.create_date.date()).days if offer.deadline and offer.create_date else 7 | ||
|
|
||
| @api.depends('validity') | ||
| def _inverse_deadline(self): | ||
| for offer in self: | ||
| # _logger.error(fields.Date.context_today(offer) + datetime.timedelta(days=offer.validity)) | ||
| # _logger.error(offer._fields) | ||
| start_date = offer.create_date.date() if offer.create_date else fields.Date.context_today(offer) | ||
| offer.deadline = start_date + datetime.timedelta(days=offer.validity) | ||
|
|
||
| # @api.constrains('price') | ||
| # def _check_price(self): | ||
| # for property in self: | ||
| # if property.price < 0: | ||
| # raise ValidationError("Value cannot be less than zero.") | ||
|
|
||
| # @api.constrains('deadline') | ||
| # def _check_deadline(self): | ||
| # for property in self: | ||
| # if property.deadline < fields.Date.context_today(property): | ||
| # raise ValidationError("Past dates not allowed") | ||
|
|
||
| def _refuse_remaining_offers(self, offer_id, all_offers): | ||
| # _logger.error(all_offers) | ||
| for offer in all_offers: | ||
| if offer.id != offer_id: | ||
| offer.status = 'refused' | ||
|
|
||
| def offer_accepted(self): | ||
| # offer_id = 0 | ||
| # breakpoint() | ||
| # for offer in self: | ||
| # if offer.status == 'accepted' and offer.property_id.state == 'offer_accepted': | ||
| # raise UserError("Property already accepted!") | ||
| # offer.status = 'accepted' | ||
| # all_offers = offer.property_id.offer_ids | ||
| # # _logger.error(all_offers) | ||
| # offer_id = offer.id | ||
| # offer._refuse_remaining_offers(offer_id, all_offers) | ||
| # offer.property_id.buyer_id = offer.partner_id | ||
| # offer.property_id.selling_price = offer.price | ||
| # offer.property_id.state = 'offer_accepted' | ||
| # return True | ||
| self.ensure_one() | ||
| property = self.property_id | ||
| if property.state == 'offer_accepted' and self.status == 'accepted': | ||
| raise ValidationError("Property already accepted!") | ||
| remaining_offers = property.offer_ids - self | ||
| remaining_offers.write({ | ||
| 'status': 'refused' | ||
| }) | ||
| self.status = 'accepted' | ||
| property.write({ | ||
| 'buyer_id': self.partner_id, | ||
| 'selling_price': self.price, | ||
| 'state': 'offer_accepted', | ||
| }) | ||
| return True | ||
|
|
||
| def offer_refused(self): | ||
| for offer in self: | ||
| if offer.status == 'refused': | ||
| raise UserError("Property already refused!") | ||
| offer.status = 'refused' | ||
| return True |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import random | ||
|
|
||
| from odoo import fields, models | ||
|
|
||
|
|
||
| class EstatePropertyTag(models.Model): | ||
| _name = 'estate.property.tag' | ||
| _description = 'Estate property tags' | ||
| _order = 'name' | ||
|
|
||
| def _get_default_color(self): | ||
| return random.randint(1, 11) | ||
|
|
||
| name = fields.Char(required=True) | ||
| color = fields.Integer(default=_get_default_color) | ||
|
|
||
| _check_name = models.Constraint( | ||
| 'UNIQUE (name)', | ||
| "Property Tag should be unique", | ||
| ) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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 purpose of using
'application': Trueand'installable': True?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.
'application': Truemakes the estate module list as an app and visible with the Apps filter, otherwise it is not listed as an app just a module'installable': Trueallows the module to be installed ('Activate' button)