diff --git a/awesome_dashboard/static/src/dashboard/component/dashboardItem.js b/awesome_dashboard/static/src/dashboard/component/dashboardItem.js new file mode 100644 index 00000000000..2a07ffc2115 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/component/dashboardItem.js @@ -0,0 +1,13 @@ +/** @odoo-module **/ + +import { Component } from "@odoo/owl"; +export class DashboardItem extends Component { + static template = "awesome_dashboard.DashBoardItem"; + static props = { + size: { + type : Number, + optional: true, + default : 1 + } + } +} diff --git a/awesome_dashboard/static/src/dashboard/component/dashboardItem.xml b/awesome_dashboard/static/src/dashboard/component/dashboardItem.xml new file mode 100644 index 00000000000..4c0d56637de --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/component/dashboardItem.xml @@ -0,0 +1,12 @@ + + + +
+
+
+ +
+
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard/component/dashboard_settings_dialog.js b/awesome_dashboard/static/src/dashboard/component/dashboard_settings_dialog.js new file mode 100644 index 00000000000..0592aa5a7e9 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/component/dashboard_settings_dialog.js @@ -0,0 +1,36 @@ +import { Component, useState } from "@odoo/owl"; +import { Dialog } from "@web/core/dialog/dialog"; + +export class DashboardSettingsDialog extends Component { + static template = "awesome_dashboard.DashboardSettingsDialog"; + static components = { Dialog }; + + static props = { + items: Array, + removedIds: Array, + onSave: Function, + close: { type: Function, optional: false }, + }; + + setup() { + this.state = useState({ + selected: new Set(this.props.items.map(i => i.id).filter(id => !this.props.removedIds.includes(id))), + }); + + this.toggleItem = (id) => { + if (this.state.selected.has(id)) { + this.state.selected.delete(id); + } else { + this.state.selected.add(id); + } + }; + } + + save() { + const unchecked = this.props.items + .map(item => item.id) + .filter(id => !this.state.selected.has(id)); + this.props.onSave(unchecked); + this.props.close(); + } +} diff --git a/awesome_dashboard/static/src/dashboard/component/dashboard_settings_dialog.xml b/awesome_dashboard/static/src/dashboard/component/dashboard_settings_dialog.xml new file mode 100644 index 00000000000..4d50b61c5ff --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/component/dashboard_settings_dialog.xml @@ -0,0 +1,21 @@ + + +
+ +
+ + +
+
+
+ + + +
+
diff --git a/awesome_dashboard/static/src/dashboard/component/numberCard.js b/awesome_dashboard/static/src/dashboard/component/numberCard.js new file mode 100644 index 00000000000..709ef9c2c13 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/component/numberCard.js @@ -0,0 +1,8 @@ +/** @odoo-module **/ + +import { Component } from "@odoo/owl"; + +export class NumberCard extends Component{ + static template = "awesome_dashboard.NumberCard"; + static props = ["title", "value"]; +} diff --git a/awesome_dashboard/static/src/dashboard/component/numberCard.xml b/awesome_dashboard/static/src/dashboard/component/numberCard.xml new file mode 100644 index 00000000000..a2d40c1bfbc --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/component/numberCard.xml @@ -0,0 +1,9 @@ + + + +
+
+

+
+
+
diff --git a/awesome_dashboard/static/src/dashboard/component/pieChart.js b/awesome_dashboard/static/src/dashboard/component/pieChart.js new file mode 100644 index 00000000000..05834fed5f4 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/component/pieChart.js @@ -0,0 +1,50 @@ +/** @odoo-module **/ + +import { loadJS } from "@web/core/assets"; +import { Component, onWillStart,useRef,onWillUpdateProps,onMounted } from "@odoo/owl"; + +export class PieChart extends Component{ + + static template = "awesome_dashboard.pie_chart" + + setup(){ + this.canvasRef = useRef("canvas") + onWillStart(async()=>{ + await loadJS("/web/static/lib/Chart/Chart.js") + }) + onMounted(() => { + this.renderChart(); + }); + onWillUpdateProps((nextProps) => { + if (this.chart) { + this.chart.destroy(); + } + this.renderChart(nextProps.data); + }); + } + + renderChart(){ + const labels = Object.keys(this.props.data); + const data = Object.values(this.props.data); + const color = ["#007BFF", "#FFA500", "#808090",]; + const ctx = this.canvasRef.el.getContext("2d"); + this.chart = new Chart(ctx,{ + type:"pie", + data:{ + labels:labels, + datasets: [ + { + data:data, + backgroundColor: color + } + ] + }, + options: { + responsive: true, + maintainAspectRatio: false, + }, + + }) + + } +} diff --git a/awesome_dashboard/static/src/dashboard/component/pieChart.xml b/awesome_dashboard/static/src/dashboard/component/pieChart.xml new file mode 100644 index 00000000000..e138f1ea925 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/component/pieChart.xml @@ -0,0 +1,11 @@ + + + +

+ +

+
+ +
+
+
diff --git a/awesome_dashboard/static/src/dashboard/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js new file mode 100644 index 00000000000..5f6165d021b --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -0,0 +1,65 @@ +/** @odoo-module **/ + +import { Component,useState} from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { Layout } from "@web/search/layout"; +import { useService } from "@web/core/utils/hooks"; +import { DashboardItem } from "./component/dashboardItem"; +import { PieChart } from "./component/pieChart"; +import { NumberCard } from "./component/numberCard"; +import { DashboardSettingsDialog } from "./component/dashboard_settings_dialog"; +class AwesomeDashboard extends Component { + static template = "awesome_dashboard.AwesomeDashboard"; + static components = {Layout, DashboardItem, PieChart, NumberCard} + + setup(){ + let staticsService = useService("awesome_dashboard.statistics"); + this.dialog = useService("dialog"); + this.display = {controlPanel: {} }; + this.action = useService("action"); + this.stats = useState(staticsService.data); + this.removedItemIds = useState(this.getRemovedItems()); + this.items = registry.category("awesome_dashboard.items").getAll(); + } + + openCustomersKanban() + { + this.action.doAction("base.action_partner_form"); + } + + openLeads() + { + this.action.doAction({ + type: 'ir.actions.act_window', + name: "Leads", + res_model: "crm.lead", + views:[ + [false,"list"], + [false,"form"] + ] + }); + + } + + get visibleItems() { + return this.items.filter(item => !this.removedItemIds.includes(item.id)); + } + + getRemovedItems() { + return JSON.parse(localStorage.getItem("awesome_dashboard.removed_items") || "[]"); + } + + openSettings() + { + this.dialog.add(DashboardSettingsDialog, { + items: this.items, + removedIds: this.removedItemIds, + onSave: (removed) => { + localStorage.setItem("awesome_dashboard.removed_items", JSON.stringify(removed)); + this.removedItemIds.splice(0, this.removedItemIds.length, ...removed); + } + }); + } +} + +registry.category("lazy_components").add("AwesomeDashboard", AwesomeDashboard); diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml new file mode 100644 index 00000000000..4dd9e6841ee --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -0,0 +1,23 @@ + + + + + + + + + + +
+ + + + + + +
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard/dashboard_items.js b/awesome_dashboard/static/src/dashboard/dashboard_items.js new file mode 100644 index 00000000000..f06a0972b92 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js @@ -0,0 +1,69 @@ + +import { registry } from "@web/core/registry"; +import { PieChart } from "./component/pieChart"; +import { NumberCard } from "./component/numberCard"; +import { _t } from "@web/core/l10n/translation"; + +const dashboardItems = [ + { + id: "nb_new_orders", + description: _t("Number of new orders"), + Component: NumberCard, + size: 1, + props: (data) => ({ + title: _t("New Orders This Month"), + value: data.nb_new_orders, + }), + }, + { + id: "total_amount", + description: _t("Total order amount"), + Component: NumberCard, + size: 1, + props: (data) => ({ + title: "Total Order Amount", + value: data.total_amount, + }), + }, + { + id: "average_quantity", + description: _t("Average t-shirt quantity"), + Component: NumberCard, + size: 1, + props: (data) => ({ + title: _t("Avg T-shirts per Order"), + value: data.average_quantity, + }), + }, + { + id: "nb_cancelled_orders", + description:_t("Cancelled Orders"), + Component: NumberCard, + size: 1, + props: (data) => ({ + title: "Cancelled Orders", + value: data.nb_cancelled_orders, + }), + }, + { + id: "average_time", + description: _t("Avg processing time"), + Component: NumberCard, + size: 2, + props: (data) => ({ + title: "Avg Time to Ship/Cancel", + value: data.average_time + " min", + }), + }, + { + id: "pie_chart", + description:_t("Orders by size"), + Component: PieChart, + size: 2, + props: (data) => ({ data: data.orders_by_size }), + }, +]; + +for (const item of dashboardItems) { + registry.category("awesome_dashboard.items").add(item.id, item); +} diff --git a/awesome_dashboard/static/src/dashboard/service/dashboardStatisticsService.js b/awesome_dashboard/static/src/dashboard/service/dashboardStatisticsService.js new file mode 100644 index 00000000000..492212754a8 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/service/dashboardStatisticsService.js @@ -0,0 +1,35 @@ +import { rpc } from "@web/core/network/rpc"; +import { registry } from "@web/core/registry"; +import { reactive } from "@odoo/owl"; + +const dashboardStatisticsService = { + + start() { + const stats = reactive({ + nb_new_orders: 0, + total_amount: 0, + average_quantity: 0, + nb_cancelled_orders: 0, + average_time: 0, + orders_by_size: { m: 0, s: 0, xl: 0 } + }); + async function fetchStatistics() { + const result = await rpc("/awesome_dashboard/statistics"); + if (result) { + Object.assign(stats, result); + } + } + + fetchStatistics(); + + setInterval(() => { + fetchStatistics(); + }, 10601000); + return { + data : stats + }; + }, + +} + +registry.category("services").add("awesome_dashboard.statistics", dashboardStatisticsService); diff --git a/awesome_dashboard/static/src/dashboard_action.js b/awesome_dashboard/static/src/dashboard_action.js new file mode 100644 index 00000000000..23e3f4dcbaf --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_action.js @@ -0,0 +1,14 @@ +/** @odoo-module **/ + +import { Component,xml } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { LazyComponent } from "@web/core/assets"; + +class AwesomeDashboardLoader extends Component { + static components = { LazyComponent }; + static template = xml` + + `; +} + +registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboardLoader); diff --git a/awesome_owl/__manifest__.py b/awesome_owl/__manifest__.py index 77abad510ef..d068840247d 100644 --- a/awesome_owl/__manifest__.py +++ b/awesome_owl/__manifest__.py @@ -33,6 +33,7 @@ 'web/static/lib/bootstrap/scss/_variables.scss', 'web/static/lib/bootstrap/scss/_maps.scss', ('include', 'web._assets_bootstrap'), + ("include", "web._assets_bootstrap_backend"), ('include', 'web._assets_core'), 'web/static/src/libs/fontawesome/css/font-awesome.css', 'awesome_owl/static/src/**/*', diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js new file mode 100644 index 00000000000..df098e50877 --- /dev/null +++ b/awesome_owl/static/src/card/card.js @@ -0,0 +1,25 @@ +import { Component, useState } from "@odoo/owl"; + +export class Card extends Component { + + static template = "awesome_owl.Card"; + static props = { + title: String, + slots: { + type : Object, + optional : true + } + }; + + setup() + { + this.state = useState({ + isOpen : true + }) + } + + toggleContent() + { + this.state.open = !this.state.open; + } +} diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml new file mode 100644 index 00000000000..590005d1ca2 --- /dev/null +++ b/awesome_owl/static/src/card/card.xml @@ -0,0 +1,20 @@ + + + +
+
+
+
+ +
+ +
+
+ +
+
+
+
+
diff --git a/awesome_owl/static/src/components/todoItem.js b/awesome_owl/static/src/components/todoItem.js new file mode 100644 index 00000000000..8b1cfd8ae34 --- /dev/null +++ b/awesome_owl/static/src/components/todoItem.js @@ -0,0 +1,16 @@ +import { Component } from "@odoo/owl"; + +export class TodoItem extends Component{ + + static template = "awesome_owl.todoItem"; + static props = { + todo: Object, + callback: {type : Function, optional: true}, + deleteTodo:{type : Function, optional: true} + } + + onClickCheckbox() + { + this.props.callback(this.props.todo.id); + } +} diff --git a/awesome_owl/static/src/components/todoItem.xml b/awesome_owl/static/src/components/todoItem.xml new file mode 100644 index 00000000000..212ae2a763b --- /dev/null +++ b/awesome_owl/static/src/components/todoItem.xml @@ -0,0 +1,16 @@ + + + +

+ + + : + + + +

+
+
diff --git a/awesome_owl/static/src/components/todoList.js b/awesome_owl/static/src/components/todoList.js new file mode 100644 index 00000000000..5de536c73da --- /dev/null +++ b/awesome_owl/static/src/components/todoList.js @@ -0,0 +1,38 @@ +/** @odoo-module **/ + +import { Component,useState } from "@odoo/owl"; +import { TodoItem } from "./todoItem"; + +export class TodoList extends Component{ + + static template = "awesome_owl.todoList"; + static components = {TodoItem}; + static props = {} + setup() + { + this.id = useState({value : 0}); + this.todos = useState([]); + this.toggle = this.toggle.bind(this); + this.deleteTodo = this.deleteTodo.bind(this); + } + + addTodo(event) + { + if(event.keyCode == 13 && (event.target.value).trim() != "") + { + this.todos.push({id : this.id.value++, description: event.target.value, isCompleted: false}); + event.target.value = ""; + } + } + + toggle(itemId){ + let todoItem = this.todos.find(todo => todo.id == itemId); + todoItem.isCompleted = !todoItem.isCompleted; + } + + deleteTodo(todoId) + { + let todoIndex = this.todos.findIndex(todo => todo.id == todoId) + this.todos.splice(todoIndex,1) + } +} diff --git a/awesome_owl/static/src/components/todoList.xml b/awesome_owl/static/src/components/todoList.xml new file mode 100644 index 00000000000..3d5bf9299e8 --- /dev/null +++ b/awesome_owl/static/src/components/todoList.xml @@ -0,0 +1,11 @@ + + + +
+ + + + +
+
+
diff --git a/awesome_owl/static/src/counter.js b/awesome_owl/static/src/counter.js new file mode 100644 index 00000000000..21dbb6e645c --- /dev/null +++ b/awesome_owl/static/src/counter.js @@ -0,0 +1,18 @@ +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++; + if(this.props.onchange) + this.props.onchange(1) + } +} diff --git a/awesome_owl/static/src/counter.xml b/awesome_owl/static/src/counter.xml new file mode 100644 index 00000000000..97031825879 --- /dev/null +++ b/awesome_owl/static/src/counter.xml @@ -0,0 +1,7 @@ + + + +

Counter:

+ +
+
\ No newline at end of file diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 657fb8b07bb..5a9ad9655b6 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,7 +1,25 @@ /** @odoo-module **/ -import { Component } from "@odoo/owl"; +import { Component, useState } from "@odoo/owl"; +import { Counter } from "./counter"; +import { Card } from "./card/card"; +import { TodoList } from "./components/todoList"; export class Playground extends Component { static template = "awesome_owl.playground"; + static components = { Counter, Card, TodoList}; + + setup() { + this.sum = useState({value:2 }) + this.addCount = this.addCount.bind(this); + } + + increment(){ + this.state.value++; + } + + addCount(count) + { + this.sum.value = this.sum.value + count; + } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..64f5aa9ad24 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -4,7 +4,18 @@
hello world + + +

Sum :

+ +

This is new title prop

+
+ + + +
+ +
- 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..6c776c9cc91 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,24 @@ +{ + "name": "Real Estate", + "category": "Real Estate/Brokerage", + "application": True, + "installable": True, + "data": [ + "security/security.xml", + "security/ir.model.access.csv", + "data/property_type.xml", + "views/estate_property_views.xml", + "views/estate_property_tag_views.xml", + "views/estate_property_offer_views.xml", + "views/estate_property_type_views.xml", + "views/estate_menus.xml", + "views/inherit_res_users_view.xml", + "report/estate_property_templates.xml", + "report/estate_property_reports.xml" + ], + "demo": [ + "demo/property.xml", + "demo/property_offer.xml", + ], + "license": "AGPL-3", +} diff --git a/estate/data/property_type.xml b/estate/data/property_type.xml new file mode 100644 index 00000000000..626ca2d306f --- /dev/null +++ b/estate/data/property_type.xml @@ -0,0 +1,22 @@ + + + + + + Residential + + + + Commercial + + + + Industrial + + + + Land + + + + diff --git a/estate/demo/property.xml b/estate/demo/property.xml new file mode 100644 index 00000000000..e76e278c2a5 --- /dev/null +++ b/estate/demo/property.xml @@ -0,0 +1,66 @@ + + + + Big Villa + new + A nice and big villa + 12345 + 2020-02-02 + 1600000 + 6 + 100 + 4 + True + True + 100000 + south + + + + Trailer home + cancelled + Home in a trailer park + 54321 + 1970-01-01 + 100000 + 120000 + 1 + 10 + 4 + False + + + + Small home + new + Home in a trailer park + 54321 + 1970-01-01 + 100000 + 120000 + 1 + 10 + 4 + False + + + diff --git a/estate/demo/property_offer.xml b/estate/demo/property_offer.xml new file mode 100644 index 00000000000..93a71446efc --- /dev/null +++ b/estate/demo/property_offer.xml @@ -0,0 +1,39 @@ + + + 10000 + 14 + + + + + + + 1500000 + 14 + + + + + + + 1500001 + 14 + + + + + + + + + + + + + + + + + + + diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..ef260eecf25 --- /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 inherited_res_users diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..2c23d1a4b94 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,183 @@ +from odoo import fields, models, api +from odoo.exceptions import UserError, ValidationError +from dateutil.relativedelta import relativedelta +from datetime import date +from odoo.tools.float_utils import float_compare + + +class EstateProperty(models.Model): + _name = "estate.property" + _description = "Estate Model" + + name = fields.Char(string="Name") + description = fields.Text(string="Description") + postcode = fields.Text(string="Postcode") + date_availability = fields.Date( + string="Date Availability", + copy=False, + default=lambda self: date.today() + relativedelta(months=3), + ) + expected_price = fields.Float(string="Expected Price") + selling_price = fields.Integer(string="Selling Price") + bedrooms = fields.Integer(string="Bedrooms") + living_area = fields.Integer(string="Living Area") + facades = fields.Integer(string="Facades") + garage = fields.Boolean(string="Garage") + garden = fields.Boolean(string="Garden") + garden_area = fields.Integer(string="Garden Area") + + garden_orientation = fields.Selection( + string="Type", + selection=[ + ("north", "North"), + ("south", "South"), + ("east", "East"), + ("west", "West"), + ], + ) + + active = fields.Boolean(default=True, string="Active") + + state = fields.Selection( + selection=[ + ("new", "New"), + ("offer_received", "Offer Received "), + ("offer_accepted", "Offer Accepted"), + ("sold", "Sold"), + ("cancelled", "Cancelled"), + ], + string="State", + default="new", + copy=False, + ) + + # property will have Many to one relation with property type since many properties can belong to one property type + + property_type_id = fields.Many2one("estate.property.type", "Property Type") + + user_id = fields.Many2one( + "res.users", + string="Salesperson", + copy=False, + ) + + partner_id = fields.Many2one( + "res.partner", + string="Buyer", + copy=False, + ) + + tag_ids = fields.Many2many("estate.property.tag", string="Tags") + + offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers") + + total_area = fields.Integer( + compute="_compute_total_property_area", string="Total Area" + ) + + best_price = fields.Integer(compute="_compute_best_price", string="Best Price") + + status = fields.Char(default="new", string="Status") + + company_id = fields.Many2one( + "res.company", + string="Company", + default=lambda self: self.env.company, + required=True, + ) + + _order = "id desc" + + _sql_constraints = [ + ( + "check_expected_price", + "CHECK(expected_price > 0)", + "Expected price must be strictly positive", + ), + ( + "check_selling_price", + "CHECK(selling_price >= 0)", + "Selling price should be positive", + ), + ] + + @api.depends("garden_area", "living_area") + def _compute_total_property_area(self): + for area in self: + area.total_area = area.garden_area + area.living_area + + @api.depends("offer_ids.price") + def _compute_best_price(self): + for record in self: + offers_list = record.mapped("offer_ids.price") + if offers_list: + record.best_price = max(offers_list) + else: + record.best_price = 0 + + # on change of garden status , update gardern area and its orientation + + @api.onchange("garden") + def _onchange_garden_status(self): + if self.garden: + self.garden_area = 10 + self.garden_orientation = "north" + return + self.garden_area = 0 + self.garden_orientation = "" + + # acts when property is sold + # In case property is cancelled it cannot be sold + def action_sell_property(self): + # dictionary for the property status + property_sell_status_dict = {"new": True, "sold": True, "cancelled": False} + for record in self: + if self.state != "offer_accepted": + raise UserError("Property has no offer yet!!!") + if property_sell_status_dict[record.status]: + record.status = "sold" + record.state = "sold" + else: + raise UserError("Cancelled property cannot be sold.") + + # action in case of cancel property button + # If property is sold than Cannot be cancelled + + def action_cancel_property_selling(self): + property_cancel_status_dict = { + "new": True, + "cancelled": True, + "sold": False, + } + for record in self: + if property_cancel_status_dict[record.status]: + record.status = "cancelled" + else: + raise UserError("Sold property cannot be cancelled.") + + # constrains for the selling price + + @api.constrains("selling_price", "expected_price") + def _check_selling_price(self): + for data in self: + # if call will come after selling price change than it will allow updated price to work + if data.selling_price <= 0: + return + + price_float_ratio = data.selling_price / data.expected_price + ratio_diffrence = float_compare(price_float_ratio, 0.9, precision_digits=2) + if ratio_diffrence == -1: + data.selling_price = 0 + raise ValidationError( + "The selling price cannot be lower than 90% of the expected price" + ) + + # delete opration for the process + + @api.ondelete(at_uninstall=False) + def _unlink_if_state_new_or_cancelled(self): + for data in self: + if not bool(data.state == "new" or data.state == "cancelled"): + raise UserError( + "Can't delete property which is not in new or cancelled state!" + ) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..d12493de002 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,114 @@ +from odoo import fields, models, api +from datetime import datetime +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 = "Offer model for the properties of real estate" + price = fields.Float() + status = fields.Selection( + selection=[("accepted", "Accepted"), ("refused", "Refused ")], + string="Status", + copy=False, + ) + + partner_id = fields.Many2one("res.partner", string="Partner") + + property_id = fields.Many2one("estate.property", string="Property") + + property_type_id = fields.Many2one( + "estate.property.type", + related="property_id.property_type_id", + store=True, + readonly=False, + ) + + validity = fields.Integer(default=7, string="Validity") + + date_deadline = fields.Date( + compute="_compute_offer_deadline", + inverse="_deadline_update", + string="Date deadline", + ) + + # constrains of sql + + _sql_constraints = [ + ("check_price", "CHECK(price > 0)", "Offered price must be strictly positive") + ] + + # order in which data is fetched + + _order = "price desc" + + # deadline will be computed based upon the validity date + @api.depends("validity") + def _compute_offer_deadline(self): + for offer in self: + if not offer.create_date: + offer.date_deadline = datetime.now() + relativedelta( + days=(offer.validity or 0) + ) + else: + offer.date_deadline = offer.create_date + relativedelta( + days=(offer.validity or 0) + ) + + # deadline date can also be changed and once this is saved validity will be updated + def _deadline_update(self): + for offer in self: + offer.validity = ( + offer.date_deadline - (offer.create_date or datetime.now()).date() + ).days + + # action for the accepting the offer + def action_offer_confirm(self): + for record in self: + # since saling price is only updated when offer is accepted therefore it validates if offer + # is already accepted than warning + + if record.property_id.selling_price > 0: + raise UserError("Offer price already accepted for the property") + + record.status = "accepted" + record.property_id.selling_price = self.price + record.property_id.partner_id = record.partner_id + record.property_id.state = "offer_accepted" + + # action for the refusal of the status + def action_offer_refuse(self): + for record in self: + if record.status == "accepted": + record.property_id.selling_price = 0 + record.property_id.partner_id = False + record.status = "refused" + + # now in case of offer creation CRUD + # self will be a proxy object , + # property_id feilds is available in vals + @api.model_create_multi + def create(self, vals): + # will check the offer value and does property has other offers which are max thw\an this one + for value in vals: + property_details = self.env["estate.property"].browse( + value.get("property_id") + ) + for property_data in property_details: + if property_data.state == "sold": + raise UserError("Cannot create offer for sold property!!!") + offers_list = property_data.mapped("offer_ids.price") + max_offer = max(offers_list, default=0) + comparison_result = float_compare( + value.get("price"), max_offer, precision_digits=2 + ) + + if comparison_result == -1: + raise UserError("Offer with a lower amount than an existing offer") + + if property_data.state == "new": + property_data.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..81b2b19aa22 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,12 @@ +from odoo import fields, models + + +class EstatePropertyTag(models.Model): + _name = "estate.property.tag" + _description = "Tag model for the estate properties" + name = fields.Char(required=True) + color = fields.Integer(default=1) + _sql_constraints = [("check_uniquness", " UNIQUE(name)", "Tag name must be unique")] + + # order on which data will be fetched + _order = "name desc" diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..12b070946ca --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,31 @@ +from odoo import fields, models, api + + +class EstatePropertyType(models.Model): + _name = "estate.property.type" + _description = "Type of properties of estate model" + name = fields.Char(required=True) + + property_ids = fields.One2many("estate.property", "property_type_id") + + offer_ids = fields.One2many( + "estate.property.offer", "property_type_id", "Offer For Property Type" + ) + + offer_count = fields.Integer(compute="_compute_offer_count") + sequence = fields.Integer("Sequence", default=1) + + # sql constrains ::: + + _sql_constraints = [ + ("check_uniquness", " UNIQUE(name)", "Type of property name must be unique") + ] + + # order on which data will be fetched + + _order = "sequence, name desc" + + @api.depends("offer_ids") + def _compute_offer_count(self): + for data in self: + data.offer_count = len(data.offer_ids) diff --git a/estate/models/inherited_res_users.py b/estate/models/inherited_res_users.py new file mode 100644 index 00000000000..3c62e3a51b7 --- /dev/null +++ b/estate/models/inherited_res_users.py @@ -0,0 +1,13 @@ +# This model inherits from res.users model + +from odoo import models, fields + + +class InheritedResUsers(models.Model): + _inherit = "res.users" + property_ids = fields.One2many( + "estate.property", + "user_id", + "Estate Property", + domain=[("state", "in", ["new", "offer_received"])], + ) diff --git a/estate/report/estate_property_reports.xml b/estate/report/estate_property_reports.xml new file mode 100644 index 00000000000..b60f6d03b7b --- /dev/null +++ b/estate/report/estate_property_reports.xml @@ -0,0 +1,23 @@ + + + Property Offers + estate.property + qweb-pdf + estate.report_property_offers + estate.report_property_offers + 'Property Offers - %s' % (object.name).replace('/','') + + report + + + + User Properties + res.users + qweb-pdf + estate.report_user_properties + estate.report_user_properties + + report + 'User Properties - %s' % (object.name) + + diff --git a/estate/report/estate_property_templates.xml b/estate/report/estate_property_templates.xml new file mode 100644 index 00000000000..a256b9b0601 --- /dev/null +++ b/estate/report/estate_property_templates.xml @@ -0,0 +1,117 @@ + + + + + + + + + diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..f586ca395d0 --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,17 @@ +id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink + +estate.access_estate_property,Estate Manager,model_estate_property,estate.estate_group_manager,1,1,1,1 + +estate.access_estate_property_type,Estate Manager,model_estate_property_type,estate.estate_group_manager,1,1,1,1 + +estate.access_estate_property_tag,Estate Manager,model_estate_property_tag,estate.estate_group_manager,1,1,1,1 + +estate.access_estate_property_offer,Estate Manager,model_estate_property_offer,estate.estate_group_manager,1,1,1,1 + +estate.access_estate_type_to_agent,Estate Agent,model_estate_property_type,estate.estate_group_user,1,0,0,0 + +estate.access_estate_property_tag_to_agent,Estate Agent,model_estate_property_tag,estate.estate_group_user,1,0,0,0 + +estate.access_estate_property_to_agent,Estate Agent,model_estate_property,estate.estate_group_user,1,1,1,0 + +access_estate_model_offer,estate.property.offer,model_estate_property_offer,base.group_user,1,1,1,1 diff --git a/estate/security/security.xml b/estate/security/security.xml new file mode 100644 index 00000000000..d08cc5690e5 --- /dev/null +++ b/estate/security/security.xml @@ -0,0 +1,43 @@ + + + Agent + + + + + Manager + + + + + + Show properties that belongs to user + + + + [ + '|', ('user_id', '=', user.id), + ('user_id', '=', False) + ] + + + + Show all properties for manager + + + + + + [(1, '=', 1)] + + + + Estate property multi-company + + + [ + '|', ('company_id', '=', False), + ('company_id', 'in', company_ids) + ] + + diff --git a/estate/tests/__init__.py b/estate/tests/__init__.py new file mode 100644 index 00000000000..1e949c2c345 --- /dev/null +++ b/estate/tests/__init__.py @@ -0,0 +1,3 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import test_estate_property diff --git a/estate/tests/test_estate_property.py b/estate/tests/test_estate_property.py new file mode 100644 index 00000000000..73704830ffc --- /dev/null +++ b/estate/tests/test_estate_property.py @@ -0,0 +1,91 @@ +from odoo.tests.common import TransactionCase +from odoo.tests import tagged, Form +from odoo.exceptions import UserError + + +@tagged("post_install", "-at_install") +class TestEstateProperty(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.property_type = cls.env["estate.property.type"].create( + {"name": "House Test Type"} + ) + + cls.properties = cls.env["estate.property"].create( + [ + { + "name": "Sale Test Property", + "description": "Test Description", + "expected_price": 100000, + "living_area": 50, + "property_type_id": cls.property_type.id, + }, + { + "name": "Garden Test Property", + "description": "Test Description Garden", + "expected_price": 200000, + "living_area": 100, + "property_type_id": cls.property_type.id, + }, + ] + ) + + cls.offers = cls.env["estate.property.offer"].create( + [ + { + "partner_id": cls.env.ref("base.res_partner_2").id, + "price": 110000, + "property_id": cls.properties[0].id, + "validity": 7, + }, + { + "partner_id": cls.env.ref("base.res_partner_12").id, + "price": 130000, + "property_id": cls.properties[0].id, + "validity": 7, + }, + { + "partner_id": cls.env.ref("base.res_partner_2").id, + "price": 150000, + "property_id": cls.properties[0].id, + "validity": 7, + }, + ] + ) + + def test_sell_property_without_accepted_offer(self): + with self.assertRaises(UserError): + self.properties[0].action_sell_property() + + def test_sold_property_cannot_create_offer(self): + self.properties[0].write({"state": "offer_accepted"}) + self.properties[0].action_sell_property() + with self.assertRaises(UserError): + self.env["estate.property.offer"].create( + [ + { + "partner_id": self.env.ref("base.res_partner_2").id, + "price": 110000, + "property_id": self.properties[0].id, + "validity": 7, + }, + ] + ) + + def test_garden_toggle(self): + with Form(self.properties[1]) as form: + form.garden = True + self.assertEqual(form.garden_area, 10, "Garden area should be reset to 10") + self.assertEqual( + form.garden_orientation, + "north", + "Garden orientation should be reset to north", + ) + form.garden = False + self.assertEqual(form.garden_area, 0, "Garden area should be reset to 0") + self.assertEqual( + form.garden_orientation, + False, + "Garden orientation should be reset to False", + ) diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..30a4276d9e5 --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..709eab0492e --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,23 @@ + + + + + Offers + estate.property.offer + list,form + + + + estate.property.list + estate.property.offer + + + + + + + + + + + \ No newline at end of file diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml new file mode 100644 index 00000000000..4678cf09687 --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,11 @@ + + + + + + Property Tag + estate.property.tag + list,form + + + \ No newline at end of file diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml new file mode 100644 index 00000000000..ac949a6b992 --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,66 @@ + + + + + + + + + Property Type + estate.property.type + list,form + + + + + estate.property.type.list + estate.property.type + + + + + + + + + + + estate.property.type.form + estate.property.type + +
+ +
+ +
+ +
+

+ +

+
+ + + + + + + + + + + +
+
+
+
+ +
\ No newline at end of file diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..fa39b826c21 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,165 @@ + + + Properties + estate.property + list,form + {'search_default_available': 1} + + + + Property Type + estate.property.type + list,form + + + + + Property Tag + estate.property.tag + list,form + + + + estate.property.list + estate.property + + + + + + + + + + + + + + + estate.property.form + estate.property + +
+
+
+ + +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +