Skip to content
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

feat: estate #169

Open
wants to merge 1 commit into
base: 18.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions awesome_owl/static/src/card/card.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/** @odoo-module */

import { Component } from "@odoo/owl";

export class Card extends Component {}

Card.template = "awesome_owl.Card";
Card.props = {
slots: {
type: Object,
shape: {
default: Object,
title: { type: Object, optional: true },
},
},
};
13 changes: 13 additions & 0 deletions awesome_owl/static/src/card/card.xml
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" owl="1">
<div class="card" style="width: 18rem;">
<div class="card-body">
<t t-if="props.slots.title">
<h5 class="card-title"><t t-slot="title"/></h5>
</t>
<p class="card-text"><t t-slot="default"/></p>
</div>
</div>
</t>
</templates>
15 changes: 15 additions & 0 deletions awesome_owl/static/src/counter/counter.js
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 });
}

increment() {
this.state.value++;
}
}
7 changes: 7 additions & 0 deletions awesome_owl/static/src/counter/counter.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="awesome_owl.Counter" owl="1">
<p>Counter: <t t-esc="state.value"/></p>
<button class="btn btn-primary" t-on-click="increment">Increment</button>
</t>
</templates>
3 changes: 3 additions & 0 deletions awesome_owl/static/src/playground.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
/** @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 }
}
8 changes: 6 additions & 2 deletions awesome_owl/static/src/playground.xml
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">

<t t-name="awesome_owl.playground">
<t t-name="awesome_owl.playground" owl="1">
<div class="p-3">
hello world
<Counter />
<Card>
<t t-set-slot="title">awesome card</t>
awesome text
</Card>
</div>
</t>

Expand Down
2 changes: 1 addition & 1 deletion awesome_owl/views/templates.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@
</html>
</template>
</data>
</odoo>
</odoo>
1 change: 1 addition & 0 deletions estate/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
24 changes: 24 additions & 0 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

{
'name': 'Estate',
'category': 'Tutorials/Estate',
'summary': 'Real estate application',
'description': "",
'depends': [
'base_setup',
],
'data': [
'security/ir.model.access.csv',
'views/estate_property_views.xml',
'views/estate_property_tag_views.xml',
'views/estate_property_type_views.xml',
'views/estate_property_offer_views.xml',
'views/res_users_views.xml',
'views/estate_menu.xml',
],
'installable': True,
'application': True,
'auto_install': False,
'license': 'OEEL-1'
}
13 changes: 13 additions & 0 deletions estate/demo/demo.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<data>
<record id="model_estate_property_action_cancel" model="ir.actions.server">
<field name="name">Mass cancel</field>
<field name="model_id" ref="estate.model_estate_property"/>
<field name="binding_model_id" ref="estate.model_estate_property"/>
<field name="binding_view_types">list</field>
<field name="state">code</field>
<field name="code">action = records.action_cancel()</field>
</record>
</data>
</odoo>
5 changes: 5 additions & 0 deletions estate/models/__init__.py
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
111 changes: 111 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
from odoo import api, models, fields
from datetime import date
from dateutil.relativedelta import relativedelta
from odoo.exceptions import UserError, ValidationError
from odoo.tools.float_utils import float_compare


class EstateProperty(models.Model):
_name = "estate.property"
_description = "Real Estate Property"

name = fields.Char("Estate Name", required=True)
description = fields.Text("Description")
postcode = fields.Char("Postalcode")
date_availability = fields.Date('Date Availability', default=lambda self: date.today() + relativedelta(months=3))
expected_price = fields.Float('Expected Price', required=True)
selling_price = fields.Float('Selling Price', readonly=True)
bedrooms = fields.Integer("Bedrooms", default=2)
living_area = fields.Integer("Living Area (sqm)")
facades = fields.Integer("Facades")
garage = fields.Boolean("Garage")
garden = fields.Boolean("Garden")
garden_area = fields.Integer("Garden Area (sqm)")
garden_orientation = fields.Selection(selection=[
('north', 'NORTH'),
('south', 'SOUTH'),
('west', 'WEST'),
('east', 'EAST')
])
active = fields.Boolean("Active", default=True)
state = fields.Selection(selection=[
('new', 'NEW'),
('offer_received', 'OFFER RECEIVED'),
('offer_accepted', 'OFFER ACCEPTED'),
('sold', 'SOLD'),
('cancelled', 'CANCELLED')
], default='new')
property_type_id = fields.Many2one('estate.property.type', string='Real Estate Type')
fres-sudo marked this conversation as resolved.
Show resolved Hide resolved
buyer_id = fields.Many2one('res.partner', string='Buyer', copy=False)
salesperson_id = fields.Many2one('res.partner', string='Salesperson', default=lambda self: self.env.user)
tag_ids = fields.Many2many('estate.property.tag', string='Real Estate Tag')
offer_ids = fields.One2many('estate.property.offer', inverse_name='property_id')
total_area = fields.Float('Total Area (sqm)', compute='_compute_total_area', readonly=True)
best_offer = fields.Float('Best Offer', compute='_compute_best_offer', readonly=True)
_order = 'id desc'

_sql_constraints = [
('expected_price', 'CHECK(expected_price > 0)',
'The expected price should be strictly greater than 0.'),
('selling_price', 'CHECK(selling_price >= 0)',
'The selling price should be zero or strictly greater than 0 if an offer is accepted.'),
]

def action_reset_to_draft(self):
for record in self:
if record.state == 'sold':
raise UserError("You cannot reset a sold property to draft.")
record.state = 'new'

def action_open_offers(self):
self.ensure_one()
return {
'name': 'Property Offer',
'views': [(self.env.ref('estate.estate_property_offer_view_list').id, 'list')],
'type': 'ir.actions.act_window',
'domain': [('id', 'in', self.offer_ids.ids)],
'res_model': 'estate.property.offer'
}

def action_cancel(self):
if self.state != 'cancelled':
self.state = 'cancelled'

def action_sold(self):
if self.state == 'cancelled':
raise UserError("You can't sold an estate marked as CANCELLED")
self.state = 'sold'

@api.ondelete(at_uninstall=False)
def on_delete(self, vals_list):
for val in vals_list:
if val and val['state'] == 'new' or val['state'] == 'cancelled':
raise UserError('You can not delete a new or cancelled property')

@api.constrains('expected_price', 'selling_price')
def _check_expected_price(self):
for record in self:
if float_compare(record.expected_price * 0.9, record.selling_price, 3) == 1 and record.selling_price != 0:
raise ValidationError("The selling price must be at least the 90% of the expected price.")

@api.depends("living_area", "garden_area")
def _compute_total_area(self):
for record in self:
record.total_area = record.living_area + (record.garden_area or 0)

@api.depends("offer_ids.price")
def _compute_best_offer(self):
for record in self:
if record.offer_ids:
record.best_offer = max(offer.price for offer in record.offer_ids)
else:
record.best_offer = 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 = None
74 changes: 74 additions & 0 deletions estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from odoo import api, models, fields
from dateutil.relativedelta import relativedelta
from odoo.exceptions import UserError
from odoo.tools.float_utils import float_compare


class EstatePropertyOffer(models.Model):
_name = "estate.property.offer"
_description = "Real Estate Property Offer"

price = fields.Float("Offer Price", required=True)
status = fields.Selection(selection=[
('accepted', 'ACCEPTED'),
('refused', 'REFUSED')
])
partner_id = fields.Many2one('res.partner', required=True)
property_id = fields.Many2one('estate.property', required=True)
validity = fields.Integer('Validity', default=7)
date_deadline = fields.Date('Date Deadline', compute='_compute_date_deadline', inverse='_inverse_date_deadline', store=True)
property_type_id = fields.Many2one(
"estate.property.type", related="property_id.property_type_id", string="Property Type", store=True
)
_order = 'price desc'

_sql_constraints = [
('price', 'CHECK(price > 0)',
'The price of an offer should be strictly grater than 0.')
]

@api.model_create_multi
def create(self, vals_list):
for val in vals_list:
if val.get("property_id") and val.get("price"):
prop = self.env["estate.property"].browse(val["property_id"])

if prop.offer_ids:
max_offer = max(prop.mapped("offer_ids.price"))
if float_compare(val["price"], max_offer, precision_rounding=0.01) <= 0:
raise UserError("The offer must be higher than %.2f" % max_offer)
prop.state = "offer_received"
return super().create(vals_list)

def action_accept(self):
for offer in self.property_id.offer_ids:
if offer.status == 'accepted':
raise UserError("An offer has already been accepted for this property.")
if self.status != 'accepted':
if self.property_id.state == 'sold':
raise UserError("You can't accept an offer for a property already sold")
self.status = 'accepted'
self.property_id.state = 'offer_accepted'
self.property_id.selling_price = self.price
self.property_id.buyer_id = self.partner_id.id
else:
raise UserError("You can't accept an offer already accepted")
fres-sudo marked this conversation as resolved.
Show resolved Hide resolved

def action_refuse(self):
if not self.status:
self.status = 'refused'
else:
raise UserError("You can't refuse an offer already refused or already accepted")

@api.depends("create_date", "validity")
def _compute_date_deadline(self):
for record in self:
create_date = record.create_date or fields.Date.context_today(record)
record.date_deadline = create_date + relativedelta(days=record.validity)

def _inverse_date_deadline(self):
for record in self:
if record.create_date:
record.validity = (record.date_deadline - record.create_date.date()).days
else:
record.validity = 0
15 changes: 15 additions & 0 deletions estate/models/estate_property_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from odoo import models, fields


class EstatePropertyTag(models.Model):
_name = "estate.property.tag"
_description = "Real Estate Property Tag"

name = fields.Char("Estate Tag", required=True)
color = fields.Integer('Color')
_order = 'name desc'

_sql_constraints = [
('name', 'UNIQUE(name)',
'The name of the tag should be unique.')
]
27 changes: 27 additions & 0 deletions estate/models/estate_property_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from odoo import models, fields


class EstatePropertyType(models.Model):
_name = "estate.property.type"
_description = "Real Estate Property Type"

name = fields.Char("Estate Type", required=True)
property_ids = fields.One2many('estate.property', inverse_name='property_type_id')
sequence = fields.Integer('Sequence', default=1, help="Used to order types on the business needs")
offer_ids = fields.One2many('estate.property.offer', inverse_name='property_type_id')
_order = 'name desc'

_sql_constraints = [
('name', 'UNIQUE(name)',
'The name of the type should be unique.')
]

def action_open_offers(self):
self.ensure_one()
return {
'name': 'Property Offer',
'views': [(False, 'list')],
'type': 'ir.actions.act_window',
'domain': [('id', 'in', self.offer_ids.ids)],
'res_model': 'estate.property.offer'
}
9 changes: 9 additions & 0 deletions estate/models/res_users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from odoo import fields, models


class ResUsers(models.Model):
_inherit = "res.users"

property_ids = fields.One2many(
"estate.property", inverse_name="salesperson_id", domain=[("state", "in", ["new", "offer_received"])]
)
5 changes: 5 additions & 0 deletions estate/security/ir.model.access.csv
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
estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1
estate.access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,1
estate.access_estate_property_tag,access_estate_property_tag,estate.model_estate_property_tag,base.group_user,1,1,1,1
estate.access_estate_property_offer,access_estate_property_offer,estate.model_estate_property_offer,base.group_user,1,1,1,1
Loading