-
Notifications
You must be signed in to change notification settings - Fork 2.9k
[ADD] estate module: Implementation of Estate Module #1110
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
32a250b
ca94372
f8ae7c6
41b6a55
48db2c6
def4d3d
fa2ac6b
0c7c7f7
332bd11
6ec0905
d71b031
1361394
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,7 @@ | ||
| /** @odoo-module **/ | ||
| import { Component } from "@odoo/owl"; | ||
|
|
||
| export class Card extends Component { | ||
| static template = "awesome_owl.Card"; | ||
| static props = ["title", "content"]; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| <templates xml:space="preserve"> | ||
| <t t-name="awesome_owl.Card"> | ||
| <div class="card d-inline-block m-2" style="width: 18rem;"> | ||
| <div class="card-body"> | ||
| <h5><t t-esc="props.title"/></h5> | ||
| <p><t t-esc="props.content"/></p> | ||
| </div> | ||
| </div> | ||
| </t> | ||
| </templates> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| /** @odoo-module **/ | ||
| import { Component, useState } from "@odoo/owl"; | ||
|
|
||
| export class Counter extends Component { | ||
| static template = "awesome_owl.Counter"; | ||
|
|
||
| setup() { | ||
| this.state = useState({ value: 0 }); | ||
| } | ||
|
|
||
| debugger; | ||
| increment() { | ||
| this.state.value++; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
|
|
||
| <templates xml:space="preserve"> | ||
|
|
||
| <t t-name="awesome_owl.Counter"> | ||
| <span class="count"> | ||
| Counter: <t t-esc="state.value"/> | ||
| </span> | ||
| <button class="btn btn-primary" t-on-click="increment">Increment</button> | ||
| </t> | ||
|
|
||
| </templates> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,9 @@ | ||
| /** @odoo-module **/ | ||
| import { Component } from "@odoo/owl"; | ||
| import { Counter } from "./counter/counter"; | ||
| import { Card } from "./card/card"; | ||
|
|
||
| export class Playground extends Component { | ||
| static template = "awesome_owl.playground"; | ||
| static components = {Counter, Card}; | ||
| } |
| 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,23 @@ | ||
| { | ||
| 'name': "Estate", | ||
| 'version': '1.0', | ||
| 'depends': ['base'], | ||
| 'author': 'Dhrudeep', | ||
| 'description': """ | ||
| This module provides functionality to manage real estate properties. | ||
| """, | ||
| 'data': [ | ||
| 'security/ir.model.access.csv', | ||
| 'views/estate_property_tag_views.xml', | ||
| 'views/estate_property_offer_views.xml', | ||
| 'views/estate_property_type_views.xml', | ||
| 'views/estate_property_views.xml', | ||
| 'views/estate_property_maintenance_requests_view.xml', | ||
| 'views/estate_investor_profile_view.xml', | ||
| 'views/estate_res_users_view.xml', | ||
| 'views/estate_menus.xml', | ||
| ], | ||
| 'category': 'Tutorial', | ||
| 'license': 'LGPL-3', | ||
| 'application': True, | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| from . import estate_property | ||
| from . import estate_property_type | ||
| from . import estate_property_tag | ||
| from . import estate_property_offer | ||
| from . import estate_property_maintenance_requests | ||
| from . import res_users | ||
| from . import estate_investor_profile |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| from odoo import fields, models | ||
|
|
||
|
|
||
| class EstatePropertyInvestor(models.Model): | ||
| _name = 'estate.property.investor' | ||
| _description = 'Estate Property investor' | ||
|
|
||
| name = fields.Many2one('res.partner', string="investor") | ||
| property_ids = fields.One2many('estate.property', 'buyer_id') |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,152 @@ | ||
| from dateutil.relativedelta import relativedelta | ||
|
|
||
| from odoo import api, fields, models | ||
| from odoo.exceptions import UserError, ValidationError | ||
|
|
||
|
|
||
| class EstateProperty(models.Model): | ||
| _name = 'estate.property' | ||
| _description = 'Estate Property Planning' | ||
| _order = 'id desc' # defaultvalue = asc | ||
|
|
||
| name = fields.Char(required=True, default="Unknown") | ||
| description = fields.Text() | ||
| postcode = fields.Integer() | ||
| date_availability = fields.Date( | ||
| "Available From", copy=False, default=lambda self: fields.Date.today() + relativedelta(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() | ||
| facades = fields.Integer() | ||
| garage = fields.Boolean() | ||
| garden = fields.Boolean() | ||
| garden_area = fields.Integer() | ||
| garden_orientation = fields.Selection( | ||
| selection=[ | ||
| ('north', "North"), | ||
| ('west', "West"), | ||
| ('east', "East"), | ||
| ('south', "South"), | ||
| ], | ||
| help="Direction for the garden" | ||
| ) | ||
| active = fields.Boolean(default=True) | ||
| state = fields.Selection( | ||
| selection=[ | ||
| ('new', "New"), | ||
| ('offer_received', "Offer Received"), | ||
| ('offer_accepted', "Offer Accepted"), | ||
| ('sold', "Sold"), | ||
| ('cancelled', "Cancelled"), | ||
| ], | ||
| string="Status", | ||
| default='new', | ||
| ) | ||
| property_type_id = fields.Many2one( | ||
| 'estate.property.type', string="Property Type") | ||
| seller_id = fields.Many2one( | ||
| 'res.users', string="Salesman", default=lambda self: self.env.user | ||
| ) | ||
| buyer_id = fields.Many2one( | ||
| 'res.partner', string="Partner/Buyer", copy=False) | ||
| tags_ids = fields.Many2many('estate.property.tag') | ||
| offer_ids = fields.One2many('estate.property.offer', 'property_id') | ||
| total_area = fields.Float(compute='_compute_total_area') | ||
|
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. Can we perform search on the
Author
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. Yes, we can perform search on computed field without using |
||
| best_price = fields.Float( | ||
| "Best offer", compute='_compute_best_price', store=True) | ||
| property_maintenance_requests = fields.One2many( | ||
| 'estate.property.maintenance.requests', 'property_id') | ||
| total_maintenance_cost = fields.Float( | ||
| compute='_compute_total_maintenance_cost', string="Total Maintenance Cost") | ||
| is_favorite = fields.Boolean(string="Favorite") | ||
| # priority = fields.Integer(string='Sequence', default=16, required=True) | ||
|
|
||
| # SQL Constraint | ||
| _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") | ||
|
|
||
| # Python Constriant | ||
dhvag-odoo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| @api.constrains('selling_price', 'expected_price') | ||
| def _check_price(self): | ||
| if self.buyer_id and self.selling_price < (self.expected_price * 0.9): | ||
| raise ValidationError( | ||
| "The selling price must be at least 90% of the expected price! You must reduce the expected price if you want to accept this offer.") | ||
|
|
||
| # Compute Methods | ||
| # Depends Decorator | ||
dhvag-odoo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| @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): | ||
| result = dict(self.env['estate.property.offer']._read_group( | ||
| [('property_id', 'in', self.ids)], | ||
| ['property_id'], | ||
| ['price:max'] | ||
| )) | ||
| for record in self: | ||
| record.best_price = result.get(record, 0) | ||
|
|
||
| @api.depends('property_maintenance_requests.cost') | ||
| def _compute_total_maintenance_cost(self): | ||
| result = dict(self.env['estate.property.maintenance.requests']._read_group( | ||
| [('property_id', 'in', self.ids)], | ||
| ['property_id'], | ||
| ['cost:sum'] | ||
| )) | ||
| for record in self: | ||
| record.total_maintenance_cost = result.get(record, 0) | ||
|
|
||
| # Onchange Decorator | ||
| @api.onchange('garden') | ||
| def _onchange_garden(self): | ||
| if self.garden: | ||
| self.garden_area = 10 | ||
| self.garden_orientation = 'north' | ||
| else: | ||
| self.garden_area = None | ||
| self.garden_orientation = None | ||
|
|
||
| # Action Methods | ||
| def action_sold(self): | ||
| if 'cancelled' in self.mapped('state'): | ||
| raise UserError("Cancelled properties cannot be sold.") | ||
dhvag-odoo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| else: | ||
| for record in self: | ||
| if record.property_maintenance_requests.status != 'done': | ||
| raise UserError( | ||
| "Property cannot be sold if there is any maintenance request not done.") | ||
| if not record.offer_ids: | ||
| raise UserError( | ||
| "Property cannot be sold without any offer.") | ||
| self.state = 'sold' | ||
|
|
||
| def action_cancel(self): | ||
| if 'sold' in self.mapped('state'): | ||
| raise UserError("Sold properties cannot be cancelled.") | ||
| return self.write({'state': 'cancelled'}) | ||
|
|
||
| def accept_offer(self): | ||
| for rec in self: | ||
| best_offer = self.env['estate.property.offer'].search([ | ||
| ('property_id', '=', rec.id), | ||
| ('price', '=', rec.best_price) | ||
| ]) | ||
| best_offer.status = 'accepted' | ||
| best_offer.action_accept() | ||
|
|
||
| # Ondelete Decorator | ||
| @api.ondelete(at_uninstall=False) | ||
| def _check_state(self): | ||
| count = self.search_count([ | ||
| ('id', 'in', self.ids), | ||
| ('state', 'in', ('offer_received', 'offer_accepted')) | ||
| ]) | ||
| if count: | ||
| raise UserError("Only new and canceled properties can be deleted.") | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| from odoo import api, fields, models | ||
| from odoo.exceptions import ValidationError | ||
|
|
||
|
|
||
| class EstatePropertyMaintenanceRequests(models.Model): | ||
| _name = 'estate.property.maintenance.requests' | ||
| _description = 'Estate Property Maintenance Requests' | ||
|
|
||
| title = fields.Char() | ||
| cost = fields.Float() | ||
| status = fields.Selection( | ||
| default='new', | ||
| copy=False, | ||
| selection=[('new', "New"), ('approved', "Approved"), ('done', "Done")], | ||
| ) | ||
| property_id = fields.Many2one('estate.property', required=True) | ||
|
|
||
| # Constraint Decorator | ||
| @api.constrains('status', 'cost') | ||
| def _check_cost(self): | ||
| if self.status == 'approved' and self.cost <= 0: | ||
| raise ValidationError( | ||
| "The cost must be greater than zero.") |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| from dateutil.relativedelta import relativedelta | ||
| from odoo import api, fields, models | ||
|
|
||
| from odoo.exceptions import UserError | ||
|
|
||
|
|
||
| class EstatePropertyOffer(models.Model): | ||
| _name = 'estate.property.offer' | ||
| _description = 'Estate Property offer' | ||
| # _order = 'price desc' | ||
|
|
||
| price = fields.Float("Price") | ||
| status = fields.Selection( | ||
| copy=False, | ||
| selection=[('accepted', "Accepted"), ('refused', "Refused")], | ||
| ) | ||
| partner_id = fields.Many2one( | ||
| 'res.partner', string="Partner", required=True) | ||
| property_id = fields.Many2one('estate.property', required=True) | ||
| validity = fields.Integer(default=7) | ||
| date_deadline = fields.Date( | ||
| compute='_compute_date_deadline', inverse='_inverse_date_deadline') | ||
| property_type_id = fields.Many2one( | ||
| related='property_id.property_type_id', store=True) | ||
|
|
||
| # SQL Constraint | ||
| _check_offer_price = models.Constraint( | ||
| 'CHECK(price > 0)', "The selling price must be positive") | ||
|
|
||
| # Compute Methods | ||
| # Depends Decorator | ||
| @api.depends('create_date', 'validity') | ||
| def _compute_date_deadline(self): | ||
| for offer in self: | ||
| date = offer.create_date.date() if offer.create_date else fields.Date.today() | ||
| offer.date_deadline = date + relativedelta(days=offer.validity) | ||
|
|
||
| def _inverse_date_deadline(self): | ||
| for record in self: | ||
| start_date = ( | ||
| record.create_date.date() if record.create_date else fields.Date.today() | ||
| ) | ||
| record.validity = (record.date_deadline - start_date).days | ||
|
|
||
| # Model Decorator | ||
| @api.model | ||
| def create(self, vals): | ||
| for vals in vals: | ||
| property_id = vals.get('property_id') | ||
| if property_id: | ||
| result = self.env['estate.property.offer']._read_group( | ||
| [('property_id', '=', property_id)], | ||
| [], | ||
| ['price:max'] | ||
| ) | ||
| max_offer = result[0][0] if result else 0.0 | ||
| if vals.get('price', 0.0) <= max_offer: | ||
| raise UserError( | ||
| "The offer must be higher than existing offers.") | ||
| self.env['estate.property'].browse( | ||
| property_id).state == 'offer_received' | ||
| return super().create(vals) | ||
|
|
||
| # Action Funcitons | ||
| def action_accept(self): | ||
| for record in self: | ||
| if record.property_id.state != 'offer_accepted': | ||
| record.status = 'accepted' | ||
| record.property_id.selling_price = record.price | ||
| record.property_id.buyer_id = self.partner_id | ||
| record.property_id.state = 'offer_accepted' | ||
|
|
||
| other_offer = record.property_id.offer_ids - self | ||
| other_offer.status = 'refused' | ||
| else: | ||
| raise UserError('One offer has already been accepted') | ||
|
|
||
| def action_refuse(self): | ||
| for record in self: | ||
| record.status = 'refused' | ||
| record.property_id.state = 'new' | ||
| record.property_id.selling_price = '0' | ||
| record.property_id.buyer_id = None |
Uh oh!
There was an error while loading. Please reload this page.