diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js new file mode 100644 index 00000000000..74ab40f9642 --- /dev/null +++ b/awesome_owl/static/src/card/card.js @@ -0,0 +1,9 @@ +import { Component } from "@odoo/owl"; + +export class Card extends Component { + static template = "awesome_owl.Card"; + static props = { + title: String, + content: String, + }; +} \ No newline at end of file diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml new file mode 100644 index 00000000000..344b348738b --- /dev/null +++ b/awesome_owl/static/src/card/card.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js new file mode 100644 index 00000000000..80d5d5ab759 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.js @@ -0,0 +1,19 @@ +import { Component, useState } from "@odoo/owl"; + +export class Counter extends Component { + static template = "awesome_owl.Counter"; + static props = { + onChange: { type: Function, optional: true } + }; + + setup() { + this.state = useState({ value: 1 }); + } + + increment() { + this.state.value = this.state.value + 1; + if (this.props.onChange) { + this.props.onChange(); + } + } +} \ No newline at end of file diff --git a/awesome_owl/static/src/counter/counter.xml b/awesome_owl/static/src/counter/counter.xml new file mode 100644 index 00000000000..df1fa5f2e3c --- /dev/null +++ b/awesome_owl/static/src/counter/counter.xml @@ -0,0 +1,11 @@ + + + + + + Counter: + Increment + + + + \ No newline at end of file diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 4ac769b0aa5..5b5b34beccf 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,5 +1,18 @@ -import { Component } from "@odoo/owl"; +import { markup, Component, useState } from "@odoo/owl"; +import { Counter } from "./counter/counter"; +import { Card } from "./card/card"; +import { TodoList } from "./todo_list/todo_list"; export class Playground extends Component { static template = "awesome_owl.playground"; -} + static components = { Counter, Card }; + static components = { Counter, Card, TodoList }; + + setup() { + this.state = useState({ sum: 2 }); + } + + incrementSum() { + this.state.sum++; + } +} \ No newline at end of file diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..92240e07629 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -1,10 +1,21 @@ - + - - hello world - - + + + Hello World + + - + Total Sum: + + + + + + + + + + \ No newline at end of file diff --git a/awesome_owl/static/src/todo_list/todo_item.js b/awesome_owl/static/src/todo_list/todo_item.js new file mode 100644 index 00000000000..4876dbdcbc7 --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_item.js @@ -0,0 +1,11 @@ +import { Component } from "@odoo/owl"; + +export class TodoItem extends Component { + static template = "awesome_owl.TodoItem"; + static props = { + todo: { + type: Object, + shape: { id: Number, description: String, isCompleted: Boolean } + } + }; +} \ No newline at end of file diff --git a/awesome_owl/static/src/todo_list/todo_item.xml b/awesome_owl/static/src/todo_list/todo_item.xml new file mode 100644 index 00000000000..05574703aab --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_item.xml @@ -0,0 +1,9 @@ + + + + + . + + + + \ No newline at end of file diff --git a/awesome_owl/static/src/todo_list/todo_list.js b/awesome_owl/static/src/todo_list/todo_list.js new file mode 100644 index 00000000000..43304308467 --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_list.js @@ -0,0 +1,25 @@ +import { Component, useState } from "@odoo/owl"; +import { TodoItem } from "./todo_item"; +import { useAutofocus } from "../utils"; + +export class TodoList extends Component { + static template = "awesome_owl.TodoList"; + static components = { TodoItem }; + + setup() { + this.nextId = 0; + this.todos = useState([]); + useAutofocus("input") + } + + addTodo(ev) { + if (ev.keyCode === 13 && ev.target.value != "") { + this.todos.push({ + id: this.nextId++, + description: ev.target.value, + isCompleted: false + }); + ev.target.value = ""; + } + } +} \ No newline at end of file diff --git a/awesome_owl/static/src/todo_list/todo_list.xml b/awesome_owl/static/src/todo_list/todo_list.xml new file mode 100644 index 00000000000..af6b4370baf --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_list.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js new file mode 100644 index 00000000000..6a5475b357e --- /dev/null +++ b/awesome_owl/static/src/utils.js @@ -0,0 +1,8 @@ +import { useRef, onMounted } from "@odoo/owl"; + +export function useAutofocus(refName) { + const ref = useRef(refName); + onMounted(() => { + ref.el.focus(); + }); +} \ No newline at end of file diff --git a/estate/__init__.py b/estate/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/estate/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 00000000000..49d8e54b37c --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,23 @@ +{ + "name": "Estate", + "version": "1.0", + "depends": ["base"], + "author": "japat", + "category": "Category", + "description": """ + Hello , Real Estate for everyone + """, + "data": [ + "security/ir.model.access.csv", + "report/estate_property_reports.xml", + "report/estate_property_templates.xml", + "views/estate_property_views.xml", + "views/estate_property_type_views.xml", + "views/estate_property_tag_views.xml", + "views/estate_menus.xml", + "views/estate_property_offer.xml", + "views/res_users_views.xml", + ], + "license": "LGPL-3", + "application": True, +} diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..9a2189b6382 --- /dev/null +++ b/estate/models/__init__.py @@ -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 diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..63c65eeb7bc --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,130 @@ +from datetime import timedelta + +from odoo import api, fields, models, _ +from odoo.exceptions import UserError, ValidationError +from odoo.tools.float_utils import float_compare, float_is_zero + + +class Property(models.Model): + _name = "estate.property" + _description = "estate property details" + _order = "id desc" + + name = fields.Char(required=True) + description = fields.Text() + postcode = fields.Char() + date_availability = fields.Date( + copy=False, default=lambda self: fields.Date.today() + timedelta(days=90) + ) + expected_price = fields.Float(required=True) + selling_price = fields.Float(readonly=True, copy=False) + 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)", store=True) + garden_orientation = fields.Selection( + selection=[ + ("north", "North"), + ("west", "West"), + ("east", "East"), + ("south", "South"), + ] + ) + active = fields.Boolean(default=True) + state = fields.Selection( + selection=[ + ("new", "New"), + ("offer_received", "Offer Received"), + ("offer_accepted", "Offer Accepted"), + ("sold", "Sold"), + ("cancelled", "Cancelled"), + ], + default="new", + copy=False, + required=True, + ) + + property_type_id = fields.Many2one("estate.property.type", string="Property Type") + salesperson_id = fields.Many2one( + "res.users", string="Salesman", default=lambda self: self.env.user + ) + buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False, readonly=True) + tag_ids = fields.Many2many("estate.property.tag", string="Property Tags") + offer_ids = fields.One2many("estate.property.offer", "property_id") + total_area = fields.Float(string="Total Area(sqm)", compute="_compute_total_area") + best_price = fields.Float( + string="Best Offer", compute="_compute_best_offer", readonly=True, copy=False + ) + _check_expected_price = models.Constraint( + "CHECK(expected_price > 0)", + "The expected price should be strictly positive", + ) + _check_selling_price = models.Constraint( + "CHECK(selling_price >= 0)", + "The selling price should be strictly positive", + ) + + @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") + def _compute_best_offer(self): + for record in self: + prices = record.offer_ids.mapped("price") + if prices: + record.best_price = max(prices) + else: + record.best_price = 0.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 + + @api.constrains("selling_price", "expected_price") + def _check_price(self): + if not float_is_zero(self.selling_price, precision_rounding=0.001) and ( + float_compare( + self.selling_price, + self.expected_price * 0.9, + precision_rounding=0.01, + ) + == -1 + ): + raise ValidationError( + "The selling price must be at least 90% of the expected price" + ) + + def action_sold_button(self): + if self.filtered(lambda x: x.state == "cancelled"): + raise UserError(_("Cancelled Property cannot be sold.")) + else: + self.state = "sold" + return { + "effect": { + "fadeout": "no", + "message": "helloo", + "type": "rainbow_man", + } + } + + def action_cancel_button(self): + if self.filtered(lambda x: x.state == "sold"): + raise UserError(_("Sold Property cannot be Cancelled.")) + self.state = "cancelled" + + @api.ondelete(at_uninstall=False) + def _unlink_check_state_before_deletion(self): + for record in self: + if record.state not in ["new", "cancelled"]: + raise UserError( + _("You can only delete properties that are 'New' or 'Cancelled'.") + ) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..f86f2bd236a --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,78 @@ +from datetime import timedelta + +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() + status = fields.Selection( + selection=[("accepted", "Accepted"), ("refused", "Refused")], + copy=False, + ) + partner_id = fields.Many2one("res.partner", required=True) + property_id = fields.Many2one("estate.property", required=True) + validity = fields.Integer(default=7, string="Validity(Days)") + date_deadline = fields.Date( + string="Deadline", + compute="_compute_date_deadline", + inverse="_inverse_date_deadline", + ) + property_type_id = fields.Many2one( + related="property_id.property_type_id", string="Property Type", store=True + ) + _check_offer_price = models.Constraint( + "CHECK(price>=0)", "Offer Price must be strictly positive" + ) + + @api.depends("validity", "date_deadline") + def _compute_date_deadline(self): + for record in self: + if record.create_date: + creation_date = record.create_date.date() + else: + creation_date = fields.Date.today() + record.date_deadline = timedelta(days=record.validity) + creation_date + + def _inverse_date_deadline(self): + for record in self: + if record.create_date: + creation_date = record.create_date.date() + else: + creation_date = fields.Date.today() + date_diff = record.date_deadline - creation_date + record.validity = date_diff.days + + def action_refuse_offer(self): + for record in self: + if record.status == "accepted": + raise UserError(_("You cant refuse an already accepted offer")) + record.status = "refused" + return True + + def action_accept_offer(self): + for record in self: + if "accepted" in record.property_id.offer_ids.mapped("status"): + raise UserError(_("An Offer has already been accepted for this offer")) + + record.status = "accepted" + record.property_id.selling_price = record.price + record.property_id.state = "offer_accepted" + record.property_id.buyer_id = record.partner_id + records = record.property_id.offer_ids.filtered(lambda x: x.id != record.id) + records.write({"status": "refused"}) + + return True + + @api.model + def create(self, vals): + for val in vals: + linked_property = self.env["estate.property"].browse(val["property_id"]) + if val["price"] < linked_property.best_price: + raise UserError("An offer with higher price already exists") + linked_property.state = "offer_received" + return super().create(vals) diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..2d6641d16c6 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,11 @@ +from odoo import fields, models + + +class EstatePropertyTags(models.Model): + _name = "estate.property.tag" + _description = "Estate Property Tags" + _tags_unique = models.Constraint("unique(name)", "Name already exist") + _order = "name asc" + + name = fields.Char(required=True) + color = fields.Integer() diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..3de89077a97 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,19 @@ +from odoo import api, fields, models + + +class EstatePropertyType(models.Model): + _name = "estate.property.type" + _description = "Estate Property Type" + _type_unique = models.Constraint("unique(name)", "Name already Exist") + _order = "sequence, name asc" + + name = fields.Char(required=True) + property_ids = fields.One2many("estate.property", "property_type_id") + sequence = fields.Integer() + offer_ids = fields.One2many("estate.property.offer", "property_type_id") + offer_count = fields.Integer(compute="_compute_offer_count") + + @api.depends("offer_ids") + def _compute_offer_count(self): + for record in self: + record.offer_count = len(record.offer_ids) diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..ef2f1b083d1 --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,7 @@ +from odoo import fields, models + + +class ResUsers(models.Model): + _inherit = "res.users" + + property_ids = fields.One2many("estate.property", "salesperson_id") diff --git a/estate/report/estate_property_reports.xml b/estate/report/estate_property_reports.xml new file mode 100644 index 00000000000..38fad22ba39 --- /dev/null +++ b/estate/report/estate_property_reports.xml @@ -0,0 +1,12 @@ + + + Property Report + estate.property + qweb-pdf + estate.report_property_template + estate.report_property_template + 'Property - %s' % (object.name) + + report + + diff --git a/estate/report/estate_property_templates.xml b/estate/report/estate_property_templates.xml new file mode 100644 index 00000000000..82768e5a5b1 --- /dev/null +++ b/estate/report/estate_property_templates.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + Expected Price + + + + Selling Price + + + + Status + + + + Salesperson + + + + + + Offers + + + + + + Price + Partner + Status + + + + + + + + + + + + + + + + + + + + + No offers have been made for this property yet. + + + + + + + + + diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..0c0b62b7fee --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -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 diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..202fe5a0e35 --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + diff --git a/estate/views/estate_property_offer.xml b/estate/views/estate_property_offer.xml new file mode 100644 index 00000000000..86bd5b4b69f --- /dev/null +++ b/estate/views/estate_property_offer.xml @@ -0,0 +1,51 @@ + + + Property Offers + estate.property.offer + list,form + + + + estate.property.offer.list + estate.property.offer + + + + + + + + + + + + + + estate.property.offer.form + estate.property.offer + + + + + + + + + + + + + + + + Property Offers + estate.property.offer + list + [('property_type_id', '=', active_id)] + + diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml new file mode 100644 index 00000000000..0cf4fbc9598 --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,18 @@ + + + Properties Tags + estate.property.tag + list,form + + + + Properties Tags + estate.property.tag + + + + + + + + diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml new file mode 100644 index 00000000000..a172adc38d8 --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,52 @@ + + + Properties Type + estate.property.type + + + + + Properties + estate.property.type + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Propertiess + estate.property.type + + + + + + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..1c61a7e5bb9 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,152 @@ + + + estate property list + estate.property + + + + + + + + + + + + + + + + + estate property form + estate.property + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + estate.property.kanban + estate.property + + + + + + + + + + + + + Expected Price: + + Best Price: + + Selling Price: + + + + + + + + + + + + estate property search + estate.property + + + + + + + + + + + + + + + + + + Properties + estate.property + list,kanban,form + {'search_default_available': 1} + + diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml new file mode 100644 index 00000000000..c157cad5283 --- /dev/null +++ b/estate/views/res_users_views.xml @@ -0,0 +1,25 @@ + + + res.users.view.form.inherit.property + res.users + + + + + + + + + + + + + + + + + + + + + diff --git a/estate_account/__init__.py b/estate_account/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/estate_account/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/estate_account/__manifest__.py b/estate_account/__manifest__.py new file mode 100644 index 00000000000..ee2385553e6 --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,16 @@ +{ + "name": "Estate Account", + "version": "1.0", + "author": "ODOO", + "category": "Category", + "description": """ + Real Estate Account Section + """, + "depends": [ + "estate", + "account", + ], + "license": "LGPL-3", + "application": False, + "installable": True, +} diff --git a/estate_account/models/__init__.py b/estate_account/models/__init__.py new file mode 100644 index 00000000000..5e1963c9d2f --- /dev/null +++ b/estate_account/models/__init__.py @@ -0,0 +1 @@ +from . import estate_property diff --git a/estate_account/models/estate_property.py b/estate_account/models/estate_property.py new file mode 100644 index 00000000000..f0527f45a6c --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,31 @@ +from odoo import Command, models + + +class EstateProperty(models.Model): + _inherit = "estate.property" + + def action_sold_button(self): + self.env["account.move"].create( + { + "partner_id": self.buyer_id.id, + "move_type": "out_invoice", + "line_ids": [ + Command.create( + { + "name": "selling price addons (6%)", + "quantity": 1, + "price_unit": self.selling_price * 0.06, + } + ), + Command.create( + { + "name": "Administrative fees", + "quantity": 1, + "price_unit": 100, + } + ), + ], + } + ) + + return super().action_sold_button()
+ +