diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js deleted file mode 100644 index c4fb245621b..00000000000 --- a/awesome_dashboard/static/src/dashboard.js +++ /dev/null @@ -1,8 +0,0 @@ -import { Component } from "@odoo/owl"; -import { registry } from "@web/core/registry"; - -class AwesomeDashboard extends Component { - static template = "awesome_dashboard.AwesomeDashboard"; -} - -registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboard); diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard.xml deleted file mode 100644 index 1a2ac9a2fed..00000000000 --- a/awesome_dashboard/static/src/dashboard.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - hello dashboard - - - diff --git a/awesome_dashboard/static/src/dashboard/dash_item_dialog/dashboard_items_dialog.js b/awesome_dashboard/static/src/dashboard/dash_item_dialog/dashboard_items_dialog.js new file mode 100644 index 00000000000..40637b0ac54 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dash_item_dialog/dashboard_items_dialog.js @@ -0,0 +1,33 @@ +import { Dialog } from "@web/core/dialog/dialog"; +import { Component } from "@odoo/owl"; +import { useService } from "@web/core/utils/hooks"; + +export class DashboardItemDialog extends Component { + static template = "awesome_dashboard.dashboard_items_dialog"; + static components = { + Dialog, + }; + + setup() { + this.service = useService("dashboard_items"); + this.allItems = this.service.getAllItems(); + this.usedIds = this.service.getUsedIds(); + } + + isSelected(id) { + return this.usedIds.includes(id); + } + + select(id) { + return () => { + let found = this.usedIds.indexOf(id); + if(found >= 0) this.usedIds.splice(found, 1); + else this.usedIds.push(id); + } + } + + apply() { + this.service.setUsedIds(this.usedIds); + this.props?.close() + } +} diff --git a/awesome_dashboard/static/src/dashboard/dash_item_dialog/dashboard_items_dialog.xml b/awesome_dashboard/static/src/dashboard/dash_item_dialog/dashboard_items_dialog.xml new file mode 100644 index 00000000000..36f9e7d3a96 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dash_item_dialog/dashboard_items_dialog.xml @@ -0,0 +1,20 @@ + + + + + + +
+ + +
+
+ + + +
+
+ +
diff --git a/awesome_dashboard/static/src/dashboard/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js new file mode 100644 index 00000000000..a207a442bed --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -0,0 +1,40 @@ +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 "./dashboard_item/dashboard_item" +import { DashboardItemDialog } from "./dash_item_dialog/dashboard_items_dialog"; + +export class AwesomeDashboard extends Component { + static template = "awesome_dashboard.AwesomeDashboard"; + + static components = { + Layout, + DashboardItem + } + + setup() { + this.items = useState(useService("dashboard_items").getUsedItems()); + this.action = useService("action") + this.stats = useState(useService("statistics").loadStatistics()); + this.dialog = useService("dialog"); + } + + openCustomers() { + this.action.doAction("base.action_partner_form") + } + + openLeads() { + this.action.doAction({ + type: 'ir.actions.act_window', + res_model: 'crm.lead', + views: [[false, 'list'], [false, 'form']] + }) + } + + openItemsDialog() { + this.dialog.add(DashboardItemDialog) + } +} + +registry.category("lazy_components").add("AwesomeDashboard", AwesomeDashboard); diff --git a/awesome_dashboard/static/src/dashboard/dashboard.scss b/awesome_dashboard/static/src/dashboard/dashboard.scss new file mode 100644 index 00000000000..6af4f389665 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.scss @@ -0,0 +1,13 @@ +.o_dashboard { + background-color: azure; + display: flex; + width: 100%; + flex-direction: row; + flex-wrap: wrap; + align-content: start; +} + +.green_value { + color: green; + font-size: 3rem; +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml new file mode 100644 index 00000000000..7e68772189f --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js new file mode 100644 index 00000000000..ee138c0d99c --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js @@ -0,0 +1,10 @@ +import { Component } from "@odoo/owl"; + +export class DashboardItem extends Component { + static template = "awesome_dashboard.dashboard_item"; + + static props = { + size: {type:Number, default: 1}, + slots: {type: Object, optional: true} + } +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.scss b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.scss new file mode 100644 index 00000000000..450ca3e0105 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.scss @@ -0,0 +1,11 @@ +.card { + display: flex; + flex-direction: column; + align-items: center; + background: white; + border-radius: 0.5rem; + border: none; + padding: 1rem; + margin: 0.5rem; + height: fit-content; +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml new file mode 100644 index 00000000000..12604f4ecc3 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml @@ -0,0 +1,10 @@ + + + + +
+ +
+
+ +
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..92e0d45a272 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js @@ -0,0 +1,68 @@ +import { PieChartCard } from "./dashboard_items/pie_char_card" +import { NumberCard } from "./dashboard_items/number_card" +import { registry } from "@web/core/registry"; + +const items = [ + { + id: "average_quantity", + description: "Average amount of t-shirt", + Component: NumberCard, + size: 1, + props: (data) => ({ + title: "Average amount of t-shirt by order this month", + value: data.average_quantity + }), + }, + { + id: "average_time", + description: "Average time", + Component: NumberCard, + size: 1, + props: (data) => ({ + title: "Average time for an order to go from 'new' to 'sent' or 'cancelled'", + value: data.average_time + }), + }, + { + id: "nb_new_orders", + description: "Number of new orders", + Component: NumberCard, + size: 1, + props: (data) => ({ + title: "Number of new orders this month", + value: data.nb_new_orders + }), + }, + { + id: "nb_cancelled_orders", + description: "Number of cancelled orders", + Component: NumberCard, + size: 1, + props: (data) => ({ + title: "Number of cancelled orders this month", + value: data.nb_cancelled_orders + }), + }, + { + id: "total_amount", + description: "Total amount of new orders", + Component: NumberCard, + size: 1, + props: (data) => ({ + title: "Total amount of new orders this month", + value: data.total_amount + }), + }, + { + id: "orders_by_size", + description: "Shirt orders by size", + Component: PieChartCard, + size: 2, + props: (data) => ({ + title: "Shirt orders by size", + value: data.orders_by_size + }), + }, + ] + +registry.category("awesome_dashboard").add("items", items); diff --git a/awesome_dashboard/static/src/dashboard/dashboard_items/number_card.js b/awesome_dashboard/static/src/dashboard/dashboard_items/number_card.js new file mode 100644 index 00000000000..ad2a98f9160 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items/number_card.js @@ -0,0 +1,10 @@ +import { Component } from "@odoo/owl"; + +export class NumberCard extends Component { + static template = "awesome_dashboard.NumberCard" + + static props = { + title: String, + value: Number + } +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard_items/number_card.xml b/awesome_dashboard/static/src/dashboard/dashboard_items/number_card.xml new file mode 100644 index 00000000000..45454142519 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items/number_card.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/awesome_dashboard/static/src/dashboard/dashboard_items/pie_char_card.js b/awesome_dashboard/static/src/dashboard/dashboard_items/pie_char_card.js new file mode 100644 index 00000000000..96bb730e390 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items/pie_char_card.js @@ -0,0 +1,43 @@ +import { Component, onWillStart, useRef, onMounted, onWillPatch, useState } from "@odoo/owl"; +import { loadJS } from "@web/core/assets" + +export class PieChartCard extends Component { + static template = "awesome_dashboard.PieChartCard" + + static props = { + title: String, + value: {type: Object, optional: true} + } + + chart = null; + + drawChart() { + if(!this.props.value) return; + + if(this.chart) { + this.chart.data.datasets[0].data = Object.values(this.props.value); + this.chart.update(); + return; + } + + this.chart = new Chart(this.canvasRef.el, { + type: 'pie', + data: { + labels: Object.keys(this.props.value), + datasets: [ + { + label: 'Shirts', + data: Object.values(this.props.value) + } + ] + } + }) + } + + setup() { + this.canvasRef = useRef("canvas"); + onWillStart(() => loadJS("/web/static/lib/Chart/Chart.js")); + onMounted(() => this.drawChart()) + onWillPatch(() => this.drawChart()) + } +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard_items/pie_chart_card.xml b/awesome_dashboard/static/src/dashboard/dashboard_items/pie_chart_card.xml new file mode 100644 index 00000000000..c6b5c12d7a3 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items/pie_chart_card.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/awesome_dashboard/static/src/dashboard/services/dashboard_items.service.js b/awesome_dashboard/static/src/dashboard/services/dashboard_items.service.js new file mode 100644 index 00000000000..77cab8c07b3 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/services/dashboard_items.service.js @@ -0,0 +1,43 @@ +import { registry } from "@web/core/registry"; +import { reactive } from "@odoo/owl"; + +const storageKey = "owl-dashboard-used-dashboard-items" + +function setUsedItems(result, allItems, usedIds) { + result.splice(0, result.length); + + for(let id of usedIds) { + let found = allItems.find((i) => i.id == id); + result.push(found); + } +} + +const dashboardItemsService = { + start() { + let fromStorage = localStorage.getItem(storageKey); + let usedIds = fromStorage ? JSON.parse(fromStorage) : []; + let allItems = registry.category("awesome_dashboard").get("items"); + let usedItems = reactive([]); + + setUsedItems(usedItems, allItems, usedIds); + + return { + getUsedItems() { + return usedItems; + }, + getAllItems() { + return allItems; + }, + getUsedIds() { + return usedIds.slice(); + }, + setUsedIds(ids) { + usedIds = ids; + localStorage.setItem(storageKey, JSON.stringify(ids)); + setUsedItems(usedItems, allItems, usedIds); + } + } + } +} + +registry.category("services").add("dashboard_items", dashboardItemsService); diff --git a/awesome_dashboard/static/src/dashboard/services/statistics.service.js b/awesome_dashboard/static/src/dashboard/services/statistics.service.js new file mode 100644 index 00000000000..f1a9f55b38d --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/services/statistics.service.js @@ -0,0 +1,30 @@ +import { registry } from "@web/core/registry"; +import { rpc } from "@web/core/network/rpc"; +import { reactive } from "@odoo/owl"; + +const fetchInterval = 3000; + +async function fetchStatistics(stats) { + const result = await rpc("/awesome_dashboard/statistics"); + + for(let entry of Object.entries(result)) { + if(stats[entry[0]] !== undefined) stats[entry[0]] = entry[1]; + } +} + +const statisticsService = { + start() { + let stats = reactive({average_quantity: 0, average_time: 0, nb_cancelled_orders: 0, nb_new_orders: 0, total_amount: 0, orders_by_size: {}}); + + fetchStatistics(stats) + setInterval(() => fetchStatistics(stats), fetchInterval); + + return { + loadStatistics() { + return stats; + } + } + } +} + +registry.category("services").add("statistics", statisticsService); diff --git a/awesome_dashboard/static/src/dashboard_action.js b/awesome_dashboard/static/src/dashboard_action.js new file mode 100644 index 00000000000..f09712a4459 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_action.js @@ -0,0 +1,18 @@ +import { Component, xml } from "@odoo/owl"; +import { LazyComponent } from "@web/core/assets"; +import { AwesomeDashboard } from "./dashboard/dashboard"; +import { registry } from "@web/core/registry"; + +class AwesomeDashboardLoader extends Component { + static components = { + LazyComponent, + AwesomeDashboard + }; + static template = xml` + + `; +} + +registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboardLoader); + + diff --git a/awesome_owl/__manifest__.py b/awesome_owl/__manifest__.py index 55002ab81de..e4fb0502d24 100644 --- a/awesome_owl/__manifest__.py +++ b/awesome_owl/__manifest__.py @@ -36,7 +36,7 @@ ('include', 'web._assets_bootstrap'), ('include', 'web._assets_core'), 'web/static/src/libs/fontawesome/css/font-awesome.css', - 'awesome_owl/static/src/**/*', + 'awesome_owl/static/src/**/*' ], }, 'license': 'AGPL-3' diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js new file mode 100644 index 00000000000..303d31cd2de --- /dev/null +++ b/awesome_owl/static/src/card/card.js @@ -0,0 +1,23 @@ +import { Component, useState } from "@odoo/owl"; + +export class Card extends Component { + static template = "awesome_owl.card"; + + static props = { + title: {type: String, validate: val => { + if(val.length === 0) return false; + + let first = val.substring(0, 1); + return first === first.toUpperCase(); + }}, + slots: {type: Object, optional: true}, + } + + setup() { + this.state = useState({open: true}); + } + + toggle(){ + 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..fa2adf3bfa9 --- /dev/null +++ b/awesome_owl/static/src/card/card.xml @@ -0,0 +1,18 @@ + + + + +
+
+
+
+ +
+ +
+ +
+
+
+ +
diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js new file mode 100644 index 00000000000..bfccd6ea4ad --- /dev/null +++ b/awesome_owl/static/src/counter/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++; + this.props?.onChange(this.state.value); + } +} diff --git a/awesome_owl/static/src/counter/counter.xml b/awesome_owl/static/src/counter/counter.xml new file mode 100644 index 00000000000..5e0ec2135c0 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.xml @@ -0,0 +1,9 @@ + + + + +

Counter:

+ +
+ +
diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 4ac769b0aa5..e2124b07784 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,5 +1,25 @@ -import { Component } from "@odoo/owl"; +import { Component, markup, 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"; + + someHtml = "
some content
" + markupedHtml = markup(this.someHtml); + + static components = { + Counter, + Card, + TodoList + }; + + setup() { + this.state = useState({ sum: 2 }); + } + + incrementSum() { + this.state.sum++; + } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..32b66270c7f 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -3,8 +3,25 @@
- hello world + hello word +
+ + + This is working + + + + + + + + + + + +

The sum :

+
diff --git a/awesome_owl/static/src/todo_item/todo_item.js b/awesome_owl/static/src/todo_item/todo_item.js new file mode 100644 index 00000000000..412bacc74a4 --- /dev/null +++ b/awesome_owl/static/src/todo_item/todo_item.js @@ -0,0 +1,19 @@ +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}}, + toggleState: Function, + removeTodo: Function + } + + complete() { + this.props.toggleState(this.props.todo.id) + } + + remove() { + this.props.removeTodo(this.props.todo.id) + } +} diff --git a/awesome_owl/static/src/todo_item/todo_item.xml b/awesome_owl/static/src/todo_item/todo_item.xml new file mode 100644 index 00000000000..68da38622b2 --- /dev/null +++ b/awesome_owl/static/src/todo_item/todo_item.xml @@ -0,0 +1,14 @@ + + + + +
+ + + . + + +
+
+ +
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..923b3468819 --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_list.js @@ -0,0 +1,38 @@ +import { Component, useState } from "@odoo/owl"; +import { TodoItem } from "../todo_item/todo_item"; +import { useAutofocus } from "../util"; + +export class TodoList extends Component { + static template = "awesome_owl.todo_list"; + + counter = 1; + + static components = { + TodoItem + } + + setup() { + this.todos = useState([]); + useAutofocus("input"); + } + + change(id) { + let found = this.todos.findIndex(t => t.id === id); + if(found >= 0) this.todos[found] = {...this.todos[found], isCompleted: !this.todos[found].isCompleted}; + } + + remove(id) { + let found = this.todos.findIndex(t => t.id === id); + if(found >= 0) this.todos.splice(found, 1); + } + + tryAdd(ev) { + if(ev.keyCode != 13) return; + + let txt = ev.srcElement.value; + if(txt === "") return; + + this.todos.push({id: this.counter++, description: txt, isCompleted: false}); + ev.srcElement.value = ""; + } +} 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..abc59016150 --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_list.xml @@ -0,0 +1,11 @@ + + + + + +
+ +
+
+ +
diff --git a/awesome_owl/static/src/util.js b/awesome_owl/static/src/util.js new file mode 100644 index 00000000000..c5a84892a3c --- /dev/null +++ b/awesome_owl/static/src/util.js @@ -0,0 +1,8 @@ +import { useRef, onMounted } from "@odoo/owl"; + +export function useAutofocus(refName) { + let ref = useRef(refName); + onMounted(() => { + ref.el?.focus() + }) +} 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..8271d22e15b --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,16 @@ +{ + 'name': 'Real Estate', + 'author': 'zavan', + 'depends': ['base'], + 'application': True, + 'license': 'LGPL-3', + 'data': [ + 'security/ir.model.access.csv', + 'views/estate_property_type_views.xml', + 'views/estate_property_tag_views.xml', + 'views/estate_property_offer_views.xml', + 'views/estate_property_views.xml', + 'views/estate_menus.xml', + 'views/res_users_views.xml' + ] +} 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..eaca1278bbd --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,92 @@ +from odoo import api, fields, models, exceptions +from dateutil.relativedelta import relativedelta +from odoo.tools.float_utils import float_compare, float_is_zero + + +class EstateProperty(models.Model): + _name = 'estate.property' + _description = "Real estate property" + _order = "id desc" + + _check_expected_price = models.Constraint('CHECK(expected_price > 0)', 'The expected price should always be positive') + _check_selling_price = models.Constraint('CHECK(selling_price >= 0)', 'The selling price should always be positive') + + name = fields.Char(required=True) + description = fields.Text() + postcode = fields.Char() + date_availability = fields.Date("Available From", copy=False, default=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'), ('south', 'South'), ('east', 'East'), ('west', 'West')]) + active = fields.Boolean(default=True) + state = fields.Selection( + selection=[('new', 'New'), ('offer-received', 'Offer Received'), ('offer-accepted', 'Offer Accepted'), ('sold', 'Sold'), ('cancelled', 'Cancelled')], + copy=False, default="new", + required=True + ) + property_type_id = fields.Many2one("estate.property.type", string="Type") + buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False, readonly=True) + salesperson_id = fields.Many2one("res.users", string="Salesperson", default=lambda self: self.env.user) + tag_ids = fields.Many2many("estate.property.tag", string="Tags") + offer_ids = fields.One2many("estate.property.offer", "property_id") + total_area = fields.Integer(compute="_compute_total_area") + best_price = fields.Float(string="Best Offer", compute="_compute_best_price") + + @api.depends("garden_area", "living_area") + def _compute_total_area(self): + for record in self: + record.total_area = record.garden_area + record.living_area + + @api.depends("offer_ids") + def _compute_best_price(self): + for prop in self: + prop.best_price = max((offer.price for offer in prop.offer_ids), default=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 + + def sell_property(self): + for prop in self: + if (prop.state == 'sold'): + continue + elif (prop.state == 'cancelled'): + raise exceptions.UserError('Cannot sell a cancelled property') + elif (prop.state != 'offer-accepted'): + raise exceptions.UserError('Cannot sell a property with no accepted offer') + else: + prop.state = 'sold' + return True + + def cancel_property(self): + for prop in self: + if (prop.state == 'cancelled'): + continue + elif (prop.state == 'sold'): + raise exceptions.UserError('Cannot cancel a sold property') + else: + prop.state = 'cancelled' + return True + + @api.constrains("selling_price", "expected_price") + def _check_price(self): + for prop in self: + if (not (prop.selling_price is None or float_is_zero(prop.selling_price, precision_digits=2)) and + float_compare(prop.selling_price, prop.expected_price * 9 / 10, precision_digits=2) < 0): + raise exceptions.ValidationError("The selling price cannot be below 90 percent of the expected price") + + @api.ondelete(at_uninstall=False) + def _unlink_if_new_or_cancelled(self): + if any(prop.state != 'new' and prop.state != 'cancelled' for prop in self): + raise exceptions.UserError("Can only delete new or cancelled properties") diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..74cd4f9c969 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,64 @@ +from odoo import api, fields, models, exceptions +from dateutil.relativedelta import relativedelta +import datetime + + +class EstatePropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "Estate property offer" + _order = "price desc" + + _check_price = models.Constraint('CHECK(price > 0)', 'The price should always be positive') + + price = fields.Float() + status = fields.Selection(readonly=True, 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) + date_deadline = fields.Date(compute="_compute_deadline", inverse="_inverse_validity", readonly=False) + property_type_id = fields.Many2one(related="property_id.property_type_id") + + @api.model + def create(self, vals_list): + prop = self.env['estate.property'].browse(vals_list[0]['property_id']) + + if (any(o.price > vals_list[0]['price'] for o in prop.offer_ids)): + raise exceptions.UserError("Cannot add an offer with a lower amount than an existing one") + + prop.state = "offer-received" + + return super().create(vals_list) + + @api.depends("validity", "create_date") + def _compute_deadline(self): + for offer in self: + create = offer.create_date.date() if isinstance(offer.create_date, datetime.datetime) else fields.Date.today() + offer.date_deadline = create + relativedelta(days=offer.validity) + + def _inverse_validity(self): + for offer in self: + create = offer.create_date.date() if isinstance(offer.create_date, datetime.datetime) else fields.Date.today() + offer.validity = (offer.date_deadline - create).days + + def accept_offer(self): + for offer in self: + if (offer.status == 'accepted'): + continue + + for other in offer.property_id.offer_ids: + if (other.status == 'accepted'): + raise exceptions.UserError("Cannot accept multiple offers for a single property") + + offer.status = 'accepted' + offer.property_id.buyer_id = offer.partner_id + offer.property_id.selling_price = offer.price + offer.property_id.state = "offer-accepted" + return True + + def refuse_offer(self): + for offer in self: + if (offer.status == 'accepted'): + offer.property_id.buyer_id = None + offer.property_id.selling_price = None + offer.status = 'refused' + return True diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..d2dbd0e6a0f --- /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 = "Estate property tag" + _order = "name" + + _unique_name = models.Constraint("UNIQUE (name)", "A tag should be unique") + + 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..bbad0fafb2d --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,20 @@ +from odoo import api, fields, models + + +class EstatePropertyType(models.Model): + _name = "estate.property.type" + _description = "Real estate property type" + _order = "name" + + _unique_name = models.Constraint("UNIQUE (name)", "A property type should be unique") + + name = fields.Char(required=True) + sequence = fields.Integer(default=1) + property_ids = fields.One2many("estate.property", "property_type_id") + 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 prop_type in self: + prop_type.offer_count = len(prop_type.offer_ids) diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..54cef1202b7 --- /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/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..e20ec4dd90b --- /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 +access_estate_property,access.estate.property,model_estate_property,base.group_user,1,1,1,1 +access_estate_property_type,access.estate.property.type,model_estate_property_type,base.group_user,1,1,1,1 +access_estate_property_tag,access.estate.property.tag,model_estate_property_tag,base.group_user,1,1,1,1 +access_estate_property_offer,access.estate.property.offer,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..1081c03c37f --- /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..61e6c51230c --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,35 @@ + + + + estate.property.offer.list + estate.property.offer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Estate Property Type + estate.property.type + list,form + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..c9e13db954c --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,134 @@ + + + + estate.property.form + estate.property + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + estate.property.list + estate.property + + + + + + + + + + + + + + + + estate.property.search + estate.property + + + + + + + + + + + + + + + + + + + estate.property.kanban + estate.property + + + + + +
+ +
+
+ Expected Price : + +
+
+ Best Price : + +
+
+ Selling Price : + +
+ +
+
+
+
+
+ + + Estate Property + estate.property + kanban,list,form + {'search_default_available': True} + +
diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml new file mode 100644 index 00000000000..1e0afe33495 --- /dev/null +++ b/estate/views/res_users_views.xml @@ -0,0 +1,15 @@ + + + + res.users.view.form.inherit.gamification + 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..8a89d41ac35 --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,6 @@ +{ + 'name': 'Real Estate Accounting', + 'author': 'zavan', + 'depends': ['estate', 'account'], + 'license': 'LGPL-3' +} 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..9d92998f846 --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,27 @@ +from odoo import models, Command + + +class EstateProperty(models.Model): + _inherit = ["estate.property"] + + def sell_property(self): + for prop in self: + vals = { + 'partner_id': prop.buyer_id.id, + 'move_type': 'out_invoice', + 'journal_id': 1, + 'invoice_line_ids': [ + Command.create({ + 'name': '6 percent of selling price', + 'quantity': 1, + 'price_unit': (prop.selling_price * 6 / 100) + }), + Command.create({ + 'name': 'Administrative fees', + 'quantity': 1, + 'price_unit': 100 + }) + ] + } + self.env['account.move'].create(vals) + return super().sell_property()