-
Notifications
You must be signed in to change notification settings - Fork 2.9k
[ADD] estate: create a new module for Real Estate Advertisement #1127
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
1d9543b
03dde4d
b6e4d06
b2ac4fa
bfd650f
3490325
b330f0e
c7b8c7f
e97d000
f7fb822
00761e0
2b04b99
476383d
af15929
39246b2
4d9b290
7e72d0a
94a7e2e
71bbdcd
c72754d
abd4ecb
c90db3e
6ce2304
b9fc1dc
d9e02b1
87989e0
6fed5bd
02774a3
e93b659
88975fc
728c4b4
7a27ef5
da2d02d
b606da3
a74c840
55edb0e
5c46462
284a46c
1709e79
daee9fb
1088fe5
25fe0a4
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 |
|---|---|---|
|
|
@@ -127,3 +127,6 @@ dmypy.json | |
|
|
||
| # Pyre type checker | ||
| .pyre/ | ||
|
|
||
| # Not functionnal | ||
| estate/security/security.xml | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| import { Component } from "@odoo/owl"; | ||
|
|
||
|
|
||
| export class Card extends Component { | ||
| static template = "awesome_owl.card"; | ||
|
|
||
| static props = { | ||
| title: {type: String,}, | ||
| content: {type: String,}, | ||
| }; | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| <?xml version="1.0" encoding="UTF-8" ?> | ||
| <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 class="card-title"><t t-esc="props.title"/></h5> | ||
| <p class="card-text"><t t-out="props.content"/></p> | ||
| </div> | ||
| </div> | ||
| </t> | ||
|
|
||
| </templates> |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,20 @@ | ||||||||||||||
| import { Component, useState } from "@odoo/owl"; | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| export class Counter extends Component { | ||||||||||||||
| static template = "awesome_owl.counter"; | ||||||||||||||
|
|
||||||||||||||
| static props = { | ||||||||||||||
| onChange: {type: Function, optional: true,}, | ||||||||||||||
| }; | ||||||||||||||
|
Comment on lines
+7
to
+9
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
|
||||||||||||||
|
|
||||||||||||||
| setup() { | ||||||||||||||
| this.state = useState({ value: 0 }); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| increment() { | ||||||||||||||
| this.state.value++; | ||||||||||||||
| if (typeof this.props.onChange === "function") | ||||||||||||||
| this.props.onChange() | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| 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"> | ||
| <div class="p-3"> | ||
| hello world | ||
| <p>Counter: <t t-esc="state.value"/></p> | ||
| <button class="btn btn-primary" t-on-click="increment">Increment</button> | ||
| </div> | ||
| </t> | ||
|
|
||
| </templates> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,27 @@ | ||
| import { Component } from "@odoo/owl"; | ||
| import { Component, useState, markup } from "@odoo/owl"; | ||
| import { Counter } from "./counter/counter"; | ||
| import { Card } from "./card/card"; | ||
| import { TodoList } from "./todo/todo_list"; | ||
| import { TodoItem } from "./todo/todo_item"; | ||
|
|
||
|
|
||
| const exampleHtml = "<div class='text-primary'>some content</div>"; | ||
|
|
||
| export class Playground extends Component { | ||
| static template = "awesome_owl.playground"; | ||
|
|
||
| static components = { Counter, Card, TodoItem, TodoList }; | ||
| // static components = {Counter, Card}; | ||
|
|
||
| html = exampleHtml; | ||
| markupHtml = markup(exampleHtml); | ||
|
|
||
| setup() { | ||
| this.state = useState({ sum: 0 }); | ||
| } | ||
|
|
||
| onChange(){ | ||
| this.state.sum ++; | ||
| } | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -2,9 +2,24 @@ | |||
| <templates xml:space="preserve"> | ||||
|
|
||||
| <t t-name="awesome_owl.playground"> | ||||
| <div class="p-3"> | ||||
| hello world | ||||
| <Counter/> | ||||
| <Counter/> | ||||
|
|
||||
| <Card title="'Card 1'" content="'content of card 1'"/> | ||||
| <Card title="'Card 2'" content="'content of card 2'"/> | ||||
|
|
||||
| <Card title="'Card 1'" content="html"/> | ||||
| <Card title="'Card 2'" content="markupHtml"/> | ||||
|
|
||||
| <div class="p-3"> | ||||
| <Counter onChange.bind="onChange"/> | ||||
| <Counter onChange.bind="onChange"/> | ||||
| <p>The sum is: <t t-esc="state.sum"/></p> | ||||
| </div> | ||||
|
|
||||
|
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
|
||||
| <!-- <TodoList todos="[{ id: 2, description: 'write tutorial', isCompleted: true }, { id: 3, description: 'buy milk', isCompleted: false }]"/> --> | ||||
| <TodoList/> | ||||
|
|
||||
| </t> | ||||
|
|
||||
| </templates> | ||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import { Component } from "@odoo/owl"; | ||
|
|
||
|
|
||
| export class TodoItem extends Component { | ||
|
|
||
| static template = "awesome_owl.todo_item"; | ||
|
|
||
| static props = { | ||
| todo: {type: Object, | ||
| shape: { | ||
| id: Number, | ||
| description: String, | ||
| isCompleted: Boolean, | ||
| }, | ||
| }, | ||
| }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| <?xml version="1.0" encoding="UTF-8" ?> | ||
| <templates xml:space="preserve"> | ||
|
|
||
| <t t-name="awesome_owl.todo_item"> | ||
| <div t-att-class="props.todo.isCompleted ? 'text-muted text-decoration-line-through' : ''"> | ||
| <t t-esc="props.todo.id"/>. <t t-esc="props.todo.description"/> | ||
| </div> | ||
| </t> | ||
|
|
||
| </templates> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import { Component, useState } from "@odoo/owl"; | ||
| import { TodoItem } from "./todo_item" | ||
|
|
||
|
|
||
| export class TodoList extends Component { | ||
| static template = "awesome_owl.todo_list"; | ||
|
|
||
| static components = { TodoItem }; | ||
|
|
||
| todos = useState([]); | ||
|
|
||
| setup() { | ||
| this.state = useState({ id_counter: 0 }); | ||
| } | ||
|
|
||
|
|
||
| addTodo(ev){ | ||
| if (ev.keyCode === 13 && ev.target.value !== ""){ | ||
| this.todos.push({ id: this.state.id_counter, description: ev.target.value, isCompleted: false }); | ||
| this.state.id_counter++; | ||
| ev.target.value = ""; | ||
| } | ||
| } | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| <?xml version="1.0" encoding="UTF-8" ?> | ||
| <templates xml:space="preserve"> | ||
|
|
||
| <t t-name="awesome_owl.todo_list"> | ||
| <div class="p-3"> | ||
| <div class="mb-2"> | ||
| <input | ||
| type="text" | ||
| class="form-control" | ||
| placeholder="Add a todo" | ||
| t-on-keyup="addTodo" | ||
| /> | ||
| </div> | ||
| <t t-foreach="todos" t-as="todo" t-key="todo.id"> | ||
| <TodoItem todo="todo"/> | ||
| </t> | ||
| </div> | ||
| </t> | ||
|
|
||
| </templates> |
| 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,18 @@ | ||||
| { | ||||
| 'name': 'Real estate', | ||||
| 'author': 'anden', | ||||
| 'license': 'LGPL-3', | ||||
| 'depends': ['base'], | ||||
| 'category': 'Real Estate/Brokerage', | ||||
| 'application': True, | ||||
| 'data': [ | ||||
| # 'security/security.xml', | ||||
|
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
|
||||
| '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/res_users_views.xml', | ||||
| 'views/estate_menus.xml', | ||||
| ] | ||||
| } | ||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| from . import property | ||
| from . import property_type | ||
| from . import property_tag | ||
| from . import property_offer | ||
| from . import res_users |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| from odoo import api, fields, models, exceptions | ||
| from odoo.tools.float_utils import float_compare, float_is_zero | ||
|
|
||
|
|
||
| class EstateProperty(models.Model): | ||
| _name = 'estate_property' | ||
| _description = 'estate property' | ||
| _order = "id desc" | ||
|
|
||
| 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) | ||
|
|
||
| _check_expected_price = models.Constraint( | ||
| 'CHECK(0 < expected_price)', | ||
| 'A property expected price must be strictly positive') | ||
|
|
||
| selling_price = fields.Float(readonly=True, copy=False) | ||
|
|
||
| _check_selling_price = models.Constraint( | ||
| 'CHECK(0 <= selling_price)', | ||
| 'A property selling price must be positive') | ||
|
|
||
| @api.constrains('selling_price', 'expected_price') | ||
| def _check_selling_price(self): | ||
| for record in self: | ||
| if not float_is_zero(record.selling_price, 2): | ||
| if float_compare(record.selling_price, 0.9 * record.expected_price, 2) == -1: | ||
| raise exceptions.ValidationError("The selling price cannot be lower than 90% of the expected price.") | ||
|
|
||
| bedrooms = fields.Integer(default=2) | ||
| living_area = fields.Integer(string='Living Area (sqm)') | ||
| facades = fields.Integer() | ||
| garage = fields.Boolean() | ||
| garden = fields.Boolean() | ||
| garden_area = fields.Integer(string='Garden Area (sqm)') | ||
| garden_orientation = fields.Selection(selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')]) | ||
| active = fields.Boolean(default=True) | ||
| 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', | ||
| ) | ||
| property_type_id = fields.Many2one('estate_property_type', string='Property Type') | ||
| salesperson = fields.Many2one('res.users', string='Salesman', default=lambda self: self.env.user) | ||
| buyer = fields.Many2one('res.partner', copy=False) | ||
| property_tag_id = fields.Many2many('estate_property_tag', string='Property Tag') | ||
| property_offer_id = fields.One2many('estate_property_offer', 'property_id', string='Property Offer') | ||
| total_area = fields.Float(string='Total Area (sqm)', compute='_compute_area') | ||
| best_price = fields.Float(string='Best offer', compute='_compute_best_price') | ||
|
|
||
| @api.depends('living_area', 'garden_area') | ||
| def _compute_area(self): | ||
| for record in self: | ||
| record.total_area = record.living_area + record.garden_area | ||
|
|
||
| @api.depends('property_offer_id.price') | ||
| def _compute_best_price(self): | ||
| for record in self: | ||
| record.best_price = max(record.property_offer_id.mapped('price'), default=False) | ||
|
|
||
| @api.onchange('garden') | ||
| def _onchange_garden(self): | ||
| for record in self: | ||
| if record.garden: | ||
| record.garden_area = 10 | ||
| record.garden_orientation = 'north' | ||
| else: | ||
| record.garden_area = 0 | ||
| record.garden_orientation = False | ||
|
|
||
| def action_property_sold(self): | ||
| if self.state == 'cancelled': | ||
| raise exceptions.UserError('Cancelled properties cannot be sold') | ||
| else: | ||
| self.state = 'sold' | ||
|
|
||
| def action_property_cancel(self): | ||
| if self.state == 'sold': | ||
| raise exceptions.UserError('Sold properties cannot be cancelled') | ||
| else: | ||
| self.state = 'cancelled' | ||
|
|
||
| @api.onchange('property_offer_id') | ||
| def _onchange_property_offer_id(self): | ||
| for record in self: | ||
| if len(record.property_offer_id) > 0: | ||
| if record.state == 'new': | ||
| record.state = 'offer received' | ||
| elif record.state != 'cancelled': | ||
| record.state = 'new' | ||
|
|
||
| @api.ondelete(at_uninstall=False) | ||
| def _unlink_for_specific_state(self): | ||
| if any(record.state not in ('new', 'cancelled') for record in self): | ||
| raise exceptions.UserError('Can\'t delete a property if its state is not \'New\' or \'Cancelled\'') |
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -0,0 +1,65 @@ | ||||
| from odoo import api, fields, models, exceptions | ||||
|
|
||||
|
|
||||
| class EstatePropertyOffer(models.Model): | ||||
| _name = 'estate_property_offer' | ||||
| _description = 'estate property offer' | ||||
| _order = "price desc" | ||||
|
|
||||
| price = fields.Float() | ||||
| status = fields.Selection(selection=[('accepted', 'Accepted'), ('refused', 'Refused')], copy=False, readonly=True) | ||||
| partner_id = fields.Many2one('res.partner', required=True, string='Partner') | ||||
| property_id = fields.Many2one('estate_property', required=True) | ||||
| validity = fields.Integer(string='Validity (days)', default=7) | ||||
| date_deadline = fields.Date(string='Deadline', compute='_compute_deadline', inverse='_inverse_deadline') | ||||
| property_type_id = fields.Many2one("estate_property_type", related="property_id.property_type_id", store=True, string="Property Type") | ||||
|
|
||||
| @api.depends('create_date', 'validity') | ||||
| def _compute_deadline(self): | ||||
| for record in self: | ||||
| if record.create_date: | ||||
| record.date_deadline = fields.Date.add(record.create_date.date(), days=record.validity) | ||||
| else: | ||||
| record.date_deadline = fields.Date.add(fields.Date.today(), days=record.validity) # If no create_date we take the date of today | ||||
|
|
||||
| def _inverse_deadline(self): | ||||
| for record in self: | ||||
| if record.create_date: | ||||
| record.validity = (record.date_deadline - record.create_date.date()).days | ||||
| else: | ||||
| record.validity = (record.date_deadline - fields.Date.today()).days # If no create_date we take the date of today | ||||
|
|
||||
| def action_status_accepted(self): | ||||
|
|
||||
|
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
|
||||
| if len(self) > 1: | ||||
| raise exceptions.UserError('Only one offer can be accepted') | ||||
|
|
||||
| if self.status != 'accepted' and 'accepted' in self.mapped('property_id.property_offer_id.status'): | ||||
| raise exceptions.UserError('Another offer is already accepted') | ||||
|
|
||||
| self.status = 'accepted' | ||||
| self.property_id.selling_price = self.price | ||||
| self.property_id.buyer = self.partner_id | ||||
| self.property_id.state = 'offer accepted' | ||||
|
|
||||
| def action_status_refused(self): | ||||
| for record in self: | ||||
| if record.status == 'accepted': | ||||
| record.property_id.selling_price = False | ||||
| record.property_id.buyer = False | ||||
| record.property_id.state = 'offer received' | ||||
| record.status = 'refused' | ||||
|
|
||||
| _check_offer_price = models.Constraint( | ||||
| 'CHECK(0 < price)', | ||||
| 'An offer price must be strictly positive') | ||||
|
Comment on lines
+53
to
+55
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 |
||||
|
|
||||
| @api.model | ||||
| def create(self, vals_list): | ||||
|
|
||||
|
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
|
||||
| for vals in vals_list: | ||||
| max_existing_price = max((offer.price for offer in self.env['estate_property'].browse(vals_list[0]['property_id']).property_offer_id), default=0) | ||||
| if vals['price'] < max_existing_price: | ||||
| raise exceptions.UserError('The offer must be higher than ' + str(max_existing_price)) | ||||
|
|
||||
| return super().create(vals_list) | ||||
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.