diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py index a1cd72893d7..59fc266fd4b 100644 --- a/awesome_dashboard/__manifest__.py +++ b/awesome_dashboard/__manifest__.py @@ -23,7 +23,10 @@ ], 'assets': { 'web.assets_backend': [ - 'awesome_dashboard/static/src/**/*', + 'awesome_dashboard/static/src/dashboard_action.js', + ], + 'awesome_dashboard.dashboard': [ + 'awesome_dashboard/static/src/dashboard/**/*', ], }, 'license': 'AGPL-3' 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/PieChart.xml b/awesome_dashboard/static/src/dashboard/PieChart.xml new file mode 100644 index 00000000000..1bfe768a1e9 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/PieChart.xml @@ -0,0 +1,7 @@ + + +
+ +
+
+
\ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js new file mode 100644 index 00000000000..77b15c07df6 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -0,0 +1,104 @@ +import { Component, useRef, useState, onWillStart, useEffect, onWillUnmount } from "@odoo/owl"; +import { ControlPanel } from "@web/search/control_panel/control_panel" +import { Layout } from "@web/search/layout"; +import { registry } from "@web/core/registry"; +import { useService } from "@web/core/utils/hooks"; +import { _t } from "@web/core/l10n/translation"; +import { AwesomeCard } from "./dashboard_card"; +import { PieChart } from "./pie_chart"; +import { DashboardSettingsDialog } from "./dashboard_settings_dialog"; + +export class AwesomeDashboard extends Component { + static template = "awesome_dashboard.AwesomeDashboard"; + + static components = { + Layout, AwesomeCard, PieChart, + }; + + static props = { + }; + + openCustomers() { + this.action.doAction("base.action_partner_form"); + } + + openLeads() { + //this.action.doAction("base.crm.lead"); + this.action.doAction({ + type: 'ir.actions.act_window', + + res_model: 'crm.lead', + }); + } + static defaultProps = { + layout: { + ControlPanel: {}, + }, + }; + openSettings() { + this.dialog.add(DashboardSettingsDialog, { + items: registry.category("awesome_dashboard").getAll(), + }); + console.log("clicked"); + } + setup() { + + this.dialog = useService("dialog"); + this.dashStat = useService("stat_dash"); + this.action = useService("action"); + this.state = useState(this.dashStat.state); + const allItems = registry.category("awesome_dashboard").getAll(); + const removedIds = JSON.parse(localStorage.getItem("dashboard_removed_items") || "[]"); + this.items = allItems.filter(item => !removedIds.includes(item.id)); + + onWillStart(async () => { + await this.dashStat.loadStatistics(); + }); + + this.data = useState(this.dashStat.state.data); + + onWillStart(async () => { + await this.dashStat.loadStatistics(); + }); + + this.notification = useService("notification"); + this.myService = useService("myService"); + + this.components = { ControlPanel } + this.contentRef = useRef("content"); + this.action = useService("action"); + + this.sharedState = useService("shared_state"); + this.sharedState.setValue("hellokey", "hello from service, shared_state, hello key") + const value = this.sharedState.getValue("hellokey"); + console.log({ "value": value }); + this.sharedState.setValue("newKey", "hello from the newKey"); + console.log({ "newKey": this.sharedState.getValue("newKey") }); + + this.showCounterValue = () => { + //this.notification.add(`counter : ${this.myService.getVal()}`); + this.myService.getVal(); + } + } +} +const myService = { + dependencies: ["notification"], + start(env, { notification }) { + let counter = 1; + setInterval(() => { + //notification.add(`Tick Tock ${counter++}`); + counter++; + console.log(counter); + }, 1000); + + return { + getVal() { + console.log(counter); + notification.add(`Tick Tock ${counter}`); + } + } + }, +}; + +registry.category("services").add("myService", myService); +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..9c203dcb85e --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.scss @@ -0,0 +1,6 @@ +.o_dashboard { + background-color: #dddddd; + width: 100%; + height: 100%; + min-height: 100%; +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml new file mode 100644 index 00000000000..aa08b8e4d2b --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + +
+
+
+ + + + + + + + + + + + + + + + +
+
+ + + diff --git a/awesome_dashboard/static/src/dashboard/dashboard_card.js b/awesome_dashboard/static/src/dashboard/dashboard_card.js new file mode 100644 index 00000000000..9ef7e39fd07 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_card.js @@ -0,0 +1,12 @@ +import { Component, useRef } from "@odoo/owl"; +import { registry } from "@web/core/registry"; + +export class AwesomeCard extends Component { + static template = "awesome_dashboard.AwesomeCard"; + static props = { + size: { Number, default: 1 }, + value: { Number, default: 1 }, + } +} + +registry.category("actions").add("awesome_dashboard.AwesomeCard", AwesomeCard); diff --git a/awesome_dashboard/static/src/dashboard/dashboard_card.scss b/awesome_dashboard/static/src/dashboard/dashboard_card.scss new file mode 100644 index 00000000000..ced9fdc2fe7 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_card.scss @@ -0,0 +1,6 @@ +.mycard:hover{ + background-color: rgb(255, 240, 222); +} +.mycard{ + background-color: white; +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard_card.xml b/awesome_dashboard/static/src/dashboard/dashboard_card.xml new file mode 100644 index 00000000000..495817855de --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_card.xml @@ -0,0 +1,20 @@ + + + +
+
+
+ +
+
+ + + + + + +
+
+
+
+
\ No newline at end of file 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..8a8ddaee9f6 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js @@ -0,0 +1,40 @@ +import { registry } from "@web/core/registry"; + +const dashboardRegistry = registry.category("awesome_dashboard"); + +dashboardRegistry.add("average_quantity", { + id: "average_quantity", + title: "Average Quantity", + props: (data) => data.average_quantity, +}); + +dashboardRegistry.add("average_time", { + id: "average_time", + title: "Average Time", + props: (data) => data.average_time, +}); + +dashboardRegistry.add("nb_cancelled_orders", { + id: "nb_cancelled_orders", + title: "Cancelled Orders", + props: (data) => data.nb_cancelled_orders, +}); + +dashboardRegistry.add("nb_new_orders", { + id: "nb_new_orders", + title: "New Orders", + props: (data) => data.nb_new_orders, +}); + +dashboardRegistry.add("total_amount", { + id: "total_amount", + title: "Total Amount", + props: (data) => data.total_amount, +}); + +dashboardRegistry.add("pie_chart", { + id: "pie_chart", + title: "Orders by Size", + type: "chart", + props: (data) => data.orders_by_size, +}); \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/dashboard_settings_dialog.js b/awesome_dashboard/static/src/dashboard/dashboard_settings_dialog.js new file mode 100644 index 00000000000..fbc35fa6acb --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_settings_dialog.js @@ -0,0 +1,38 @@ +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 }; + + setup() { + const removed = JSON.parse( + localStorage.getItem("dashboard_removed_items") || "[]" + ); + + this.state = useState({ + items: this.props.items.map(item => ({ + ...item, + checked: !removed.includes(item.id), + })), + }); + } + + toggle(item) { + item.checked = !item.checked; + } + + apply() { + const removedIds = this.state.items + .filter(i => !i.checked) + .map(i => i.id); + + localStorage.setItem( + "dashboard_removed_items", + JSON.stringify(removedIds) + ); + + this.props.close(); + window.location.reload(); + } +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/dashboard_settings_dialog.xml b/awesome_dashboard/static/src/dashboard/dashboard_settings_dialog.xml new file mode 100644 index 00000000000..1bec2987541 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_settings_dialog.xml @@ -0,0 +1,22 @@ + + + + + + + + + \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/pie_chart.js b/awesome_dashboard/static/src/dashboard/pie_chart.js new file mode 100644 index 00000000000..871ab20d1f7 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart.js @@ -0,0 +1,52 @@ +import { onWillStart, useRef, onMounted, useEffect, Component } from "@odoo/owl"; +import { loadJS } from "@web/core/assets"; +export class PieChart extends Component { + static template = "awesome_dashboard.PieChart"; + static props = { data: { type: Object } }; + + setup() { + this.canvasRef = useRef("canvas"); + this.chart = null; + + onWillStart(async () => { + await loadJS("/web/static/lib/Chart/Chart.js"); + }); + + onMounted(() => { + this._renderChart(); + }); + + useEffect(() => { + this._updateChart(); + }, () => [this.props.data]); + } + + _renderChart() { + const ctx = this.canvasRef.el.getContext("2d"); + this.chart = new Chart(ctx, { + type: "pie", + data: { + labels: ["M", "S", "XL"], + datasets: [{ + data: [ + this.props.data.m || 0, + this.props.data.s || 0, + this.props.data.xl || 0 + ], + backgroundColor: ["#fff71b", "#15ff00", "#1b94ff"] + }], + }, + }); + } + + _updateChart() { + if (this.chart) { + this.chart.data.datasets[0].data = [ + this.props.data.m || 0, + this.props.data.s || 0, + this.props.data.xl || 0 + ]; + this.chart.update(); + } + } +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/statistics_service.js b/awesome_dashboard/static/src/dashboard/statistics_service.js new file mode 100644 index 00000000000..572869bc078 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/statistics_service.js @@ -0,0 +1,26 @@ + +import { registry } from "@web/core/registry"; +import { reactive } from "@odoo/owl"; +import { rpc } from "@web/core/network/rpc"; + +export const dashStatService = { + start(env) { + const state = reactive({ + data: {}, + }); + + async function loadStatistics() { + const result = await rpc("/awesome_dashboard/statistics"); + Object.assign(state.data, result); + return state.data; + } + + setInterval(loadStatistics, 1000); + + return { + state, + loadStatistics, + }; + }, +}; +registry.category("services").add("stat_dash", dashStatService); \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/val_service.js b/awesome_dashboard/static/src/dashboard/val_service.js new file mode 100644 index 00000000000..76e4057f10d --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/val_service.js @@ -0,0 +1,17 @@ +import { registry } from "@web/core/registry"; + +const sharedStateService = { + start(env) { + let state = {}; + return { + getValue(key) { + return state[key]; + }, + setValue(key, value) { + state[key] = value; + }, + }; + }, +}; + +registry.category("services").add("shared_state", sharedStateService); \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard_action.js b/awesome_dashboard/static/src/dashboard_action.js new file mode 100644 index 00000000000..9404e5fe9f3 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_action.js @@ -0,0 +1,14 @@ +import { Component, xml } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { LazyComponent } from "@web/core/assets"; + +export class AwesomeDashboardLoader extends Component { + static components = { LazyComponent }; + static template = xml` + `; +} + +registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboardLoader); \ No newline at end of file diff --git a/awesome_owl/controllers/controllers.py b/awesome_owl/controllers/controllers.py index bccfd6fe283..aeac8137f0e 100644 --- a/awesome_owl/controllers/controllers.py +++ b/awesome_owl/controllers/controllers.py @@ -8,3 +8,10 @@ def show_playground(self): Renders the owl playground page """ return request.render('awesome_owl.playground') + + @http.route(['/counter'], type='http', auth='public') + def show_Counter(self): + """ + Renders the owl playground page + """ + return request.render('awesome_owl.counter') diff --git a/awesome_owl/static/src/card.js b/awesome_owl/static/src/card.js new file mode 100644 index 00000000000..291b3156731 --- /dev/null +++ b/awesome_owl/static/src/card.js @@ -0,0 +1,42 @@ +import { Component, useRef, useState } from "@odoo/owl"; + +export class Card extends Component { + static template = "awesome_owl.card"; + static props = { + title: String, text: String, + obj: Object, + handledone: Function, + toggle: Function, + newAdd: Function, + handledelete: Function, + } + setup() { + //this.newadd(this.newtask_id) + this.newtask_id = useRef('newtask_id'); + + this.handle_the_done = (param) => { + if (this.props.handledone) { + this.props.handledone(param); + } + else { + alert("the handle done func is not given"); + } + } + + this.state = useState({ 'inputnewadd': "" }) + + this.newadd_1 = () => { + + + this.props.newAdd(this.state.inputnewadd); + } + + this.handle_the_delete = (param) => { + this.props.handledelete(param); + } + + + console.log({ "test": this.newtask_id }); + + } +} diff --git a/awesome_owl/static/src/card.xml b/awesome_owl/static/src/card.xml new file mode 100644 index 00000000000..ddc858d51ac --- /dev/null +++ b/awesome_owl/static/src/card.xml @@ -0,0 +1,37 @@ + + + +
+
+
+ +
+
+ +
+
+
+ + +
+ +
+ +
+
+ +
+ + +
+
+
+
+
+
+
\ No newline at end of file diff --git a/awesome_owl/static/src/counter.js b/awesome_owl/static/src/counter.js new file mode 100644 index 00000000000..c71b53efd0f --- /dev/null +++ b/awesome_owl/static/src/counter.js @@ -0,0 +1,24 @@ +import { Component, useState } from "@odoo/owl"; + +export class Counter extends Component { + static template = "awesome_owl.counter"; + + setup() { + this.state = useState({ value: 0 , sum: 0}); + this.newstate = useState({ newval: 1 }); + } + + increment() { + this.state.value++; + console.log(this.state.value); + } + + multiple() { + this.newstate.newval = this.newstate.newval*2; + console.log(this.newstate.newval); + } + + sum() { + this.state.sum = this.state.value + this.newstate.newval + } +} diff --git a/awesome_owl/static/src/counter.xml b/awesome_owl/static/src/counter.xml new file mode 100644 index 00000000000..12494922c65 --- /dev/null +++ b/awesome_owl/static/src/counter.xml @@ -0,0 +1,11 @@ + + + +

SUM:

+

Counter:


+

Multiplex2:

+ + + +
+
diff --git a/awesome_owl/static/src/main.js b/awesome_owl/static/src/main.js index 1aaea902b55..35cfa4cda9c 100644 --- a/awesome_owl/static/src/main.js +++ b/awesome_owl/static/src/main.js @@ -7,6 +7,5 @@ const config = { name: "Owl Tutorial" }; -// Mount the Playground component when the document.body is ready whenReady(() => mountComponent(Playground, document.body, config)); diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 4ac769b0aa5..d93bc0e52d0 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,5 +1,85 @@ -import { Component } from "@odoo/owl"; +import { Component, useState, onMounted } from "@odoo/owl"; +import { Counter } from "./counter"; +import { Card } from "./card"; export class Playground extends Component { static template = "awesome_owl.playground"; + static components = { Counter, Card } + + setup() { + this.taskDone_2 = (param) => { + this.obj.tasks.find(t => t.task == param ? t.status = true : false); + + console.log({ "tasks": this.obj.tasks }); + }; + + this.toggle = (param) => { + console.log(param); + //debugger; + + /* ------------------------------------------------------------------------------------- + dont work, ! flip dont work + but the asignment works + */ + //this.obj.tasks.find(t => t.task == param ? t.status.set = !t.status : t.status = t.status) + //------------------------------------------------------------------------------------- + + /** ------------------------------------------------------------------------------------- + * works because the flip woked inside the considition matching place, + * but dont work in the body, + * and at the end it returns true because the asignment happed , and executd so + */ + this.obj.tasks.find(t => t.task == param && (t.status = !t.status)); + //------------------------------------------------------------------------------------- + + /**------------------------------------------------------------------------------------- + * this already works inside the map, + * map is mostly used for such types of execution + */ + //this.obj.tasks.map(t => t.task == param ? t.status = !t.status : t.status = t.status) + //------------------------------------------------------------------------------------- + console.log({ "tasks": this.obj.tasks }); + } + + this.newAdd = (param) => { + this.obj.tasks = [...this.obj.tasks, { "task": param, "status": false }]; + } + + this.handledelete = (param) => { + this.obj.temp = []; + + const obj = this.obj.tasks + + for (const tasks of obj) { + + if (tasks.task == param) { + this.obj.temp = [...this.obj.temp] + } + else { + this.obj.temp = [...this.obj.temp, { "task": tasks.task, "status": tasks.status }]; + } + } + this.obj.tasks = []; + + this.obj.tasks = [...this.obj.temp]; + console.log({ "temp": this.obj.temp }); + console.log({ "tasks": this.obj.tasks }); + this.obj.temp = [{}]; + } + + this.obj = useState({ tasks: [{ type: Object, write: true }], temp: [{ type: Object }] }); + this.obj.tasks = [ + { 'task': "add_color", "status": true }, + { 'task': "code0", "status": true }, + { 'task': "code1", "status": true }, + { 'task': "code2", "status": false }, + { 'task': "code3", "status": false }, + { 'task': "code4", "status": false }, + { 'task': "code5", "status": false } + ] + + console.log(this); + } + + } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..46539ad746f 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -1,10 +1,31 @@ - -
- hello world +
+
Viraj Warhade
+
VIWAR-ODOO
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
- diff --git a/awesome_owl/views/counter_template.xml b/awesome_owl/views/counter_template.xml new file mode 100644 index 00000000000..43fd45bf7e9 --- /dev/null +++ b/awesome_owl/views/counter_template.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/awesome_owl/views/templates.xml b/awesome_owl/views/templates.xml index aa54c1a7241..3557a50dab2 100644 --- a/awesome_owl/views/templates.xml +++ b/awesome_owl/views/templates.xml @@ -1,15 +1,32 @@ - 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..3e64de231b7 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,28 @@ +{ + 'name': 'Real Estate', + 'version': '1.0', + 'depends': ['base','mail'], + 'author': 'viwar-odoo', + 'category': 'real estate', + 'description': "real estate App.", + "data": [ + "security/security_groups.xml", + "security/ir.model.access.csv", + "views/estate_maintainance_form.xml", + "views/estate_property_inherited.xml", + "views/estate_property_visit_calender.xml", + "views/estate_propert_offer_wizard.xml", + "views/estate_property_visit_kanban_view.xml", + "views/estate_property_offer_view.xml", + "views/estate_property_tag_view.xml", + "views/estate_property_type_view.xml", + "views/estate_property_visit_action.xml", + "views/estate_property_view.xml", + "views/estate_menus.xml", + # "data/estate_property_demo.xml" + ], + 'application': True, + 'installable': True, + 'license': 'LGPL-3', + 'website': 'https://odoo.com', +} diff --git a/estate/data/estate_property_demo.xml b/estate/data/estate_property_demo.xml new file mode 100644 index 00000000000..423b7e57e6a --- /dev/null +++ b/estate/data/estate_property_demo.xml @@ -0,0 +1,49 @@ + + + + + + + 1 + tag1 + + + + 1 + type1 + + + + 1 + property + + 1 + + + + 1 + 100000 + 1 + + + + + + + + Marc Demo + + YourCompany + 3575 Buena Vista Avenue + Eugene + + 97401 + + Europe/Brussels + mark.brown23@example.com + (441)-695-2334 + + + + + diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..1326ed3caea --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1,8 @@ +from . import estate_maintainance_request +from . import estate_property +from . import estate_property_offer +from . import estate_property_offer_wizard +from . import estate_property_tag +from . import estate_property_type +from . import estate_property_visit +from . import inherited \ No newline at end of file diff --git a/estate/models/estate_maintainance_request.py b/estate/models/estate_maintainance_request.py new file mode 100644 index 00000000000..4b8f11ec6b2 --- /dev/null +++ b/estate/models/estate_maintainance_request.py @@ -0,0 +1,37 @@ +from odoo import api, models, fields + + +class EstateMaintainanceRequest(models.Model): + _name = 'estate.maintainance.request' + _description = 'table for the technician maintainance request' + + property_id = fields.Many2one('estate.property', required=True, readonly=True) + buyer_id = fields.Many2one('res.users', required=True, default='self.env.uid') + technician_id = fields.Many2one('res.partner', required=False) + state = fields.Selection( + string='state', + default='new', + selection=[ + ('new', "New"), + ('assigned', "Assigned"), + ('inprogress', "Inprogress"), + ('done', "Done"), + ('cancelled', "Cancelled"), + ], + ) + estimate_cost = fields.Float(required=False) + actual_cost = fields.Float(compute='_compute_actual_cost', store=True) + current_stage = fields.Char(compute='_compute_state_after_assigned', store=True) + + @api.depends('state', 'estimate_cost') + def _compute_actual_cost(self): + if self.state == 'done': + self.actual_cost = self.estimate_cost * 1.18 + + @api.depends('technician_id', 'state') + def _compute_state_after_assigned(self): + if self.state == 'new' and self.technician_id: + self.state = 'assigned' + self.current_stage = 'assigned' + else: + self.current_stage = self.state diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..be99fe42ba3 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,212 @@ +from odoo import api, fields, models, _ +from odoo.exceptions import UserError, ValidationError +from odoo.tools.float_utils import float_compare + + +class EstateProperty(models.Model): + _name = 'estate.property' + _description = 'Test Model for real estate' + _check_positive_expected_price = models.Constraint( + 'CHECK (expected_price >= 0)', 'expected_price should be positive' + ) + _check_positive_selling_price = models.Constraint( + 'CHECK (selling_price >= 0)', 'selling_price should be positive' + ) + _order = 'id desc' + _inherit = ['mail.thread'] + + name = fields.Char(default='Unknown', tracking=True) + last_seen = fields.Datetime('Last Seen', default=fields.Datetime.now) + description = fields.Text() + postcode = fields.Char() + date_availability = fields.Date( + copy=False, default=fields.Date.add(fields.Date.today(), months=3) + ) + expected_price = fields.Float() + selling_price = fields.Float(copy=False, readonly=True, default=0) + bedrooms = fields.Integer(default=2) + living_area = fields.Integer() + facades = fields.Integer() + garage = fields.Boolean() + garden = fields.Boolean() + state = fields.Selection( + string='status', + selection=[ + ('new', "New"), + ('offer received', "Offer Received"), + ('offer accepted', "Offer Accepted"), + ('sold', "Sold"), + ('cancelled', "Cancelled"), + ], + default='new', + compute='_compute_statusbar', + store=True, + tracking=True, + ) + active = fields.Boolean(default=True) + garden_area = fields.Integer() + garden_orientation = fields.Selection( + string='garden orientation direction', + selection=[ + ('north', "North"), + ('south', "South"), + ('east', "East"), + ('west', "West"), + ], + ) + property_type_id = fields.Many2one('estate.property.type') + salesperson_id = fields.Many2one( + 'res.users', + string='Salesperson', + index=True, + default=lambda self: self.env.user, + ) + buyer_id = fields.Many2one('res.partner', default='None', copy=False) + tag_ids = fields.Many2many('estate.property.tag') + offer_ids = fields.One2many('estate.property.offer', 'property_id', string='offers') + total_area = fields.Float(compute='_compute_total_area') + max_offer_price = fields.Float( + default=None, compute='_compute_max_offer_price', store=True + ) + estate_maintainance_id = fields.One2many( + 'estate.maintainance.request', 'property_id' + ) + visit_ids = fields.One2many('estate.property.visit', 'property_id', string='visits') + + @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.onchange('garden') + def _onchange_garden(self): + if self.garden: + self.garden_area = 10 + self.garden_orientation = 'north' + else: + self.garden_area = None + self.garden_orientation = None + + def button_cancel(self): + if self.state == 'cancelled': + raise UserError('The property is already cancelled') + elif self.state == 'sold': + raise UserError('The property is already sold, you cannot cancel it') + else: + self.state = 'cancelled' + + def button_sold(self): + if self.state == 'sold': + raise UserError('The property is already sold') + elif self.state == 'cancelled': + raise UserError('The property is already cancelled, and cannot be sold') + else: + self.state = 'sold' + + subject = _('Property Sold: %s') % self.name + body = _(''' +

Hello,

+

The property %s has been successfully sold for %s.

+ + + + + + + + + + + + + + +
RoleName
Seller%s
Buyer%s
+ +

This is an automated message.

+ ''') % ( + self.name, + self.selling_price, + self.salesperson_id.name, + self.buyer_id.name or 'N/A', + ) + mail_adrs = [] + if self.salesperson_id: + mail_adrs.append(self.salesperson_id.partner_id.id) + if self.buyer_id: + mail_adrs.append(self.buyer_id.id) + + ctx = { + 'default_model': 'estate.property', + 'default_res_ids': self.ids, + 'default_composition_mode': 'comment', + 'default_email_layout_xmlid': 'mail.mail_notification_layout_with_responsible_signature', + 'default_subject': subject, + 'default_body': body, + 'default_partner_ids': mail_adrs, + 'email_notification_allow_footer': True, + 'hide_mail_template_management_options': False, + } + + return { + 'name': _('Send Confirmation Email'), + 'type': 'ir.actions.act_window', + 'res_model': 'mail.compose.message', + 'view_mode': 'form', + 'views': [(False, 'form')], + 'view_id': False, + 'target': 'new', + 'context': ctx, + } + + @api.depends('offer_ids.price') + def _compute_max_offer_price(self): + for rec in self: + if rec.offer_ids: + new_max_offer_price = max(rec.offer_ids.mapped('price')) + if rec.max_offer_price != new_max_offer_price: + rec.max_offer_price = new_max_offer_price + + def accept_best_offer(self): + for offers in self.offer_ids: + if self.max_offer_price == offers.price: + offers.action_accept_offer() + ################################## + # old code + # offers.status = 'accepted' + # self.state = 'offer accepted' + # self.buyer_id = offers.partner_id + # self.selling_price = offers.price + ################################## + else: + offers.status = 'refused' + + @api.depends('offer_ids') + def _compute_statusbar(self): + for record in self: + if record.offer_ids and record.state == 'new': + record.state = 'offer received' + + @api.constrains('selling_price', 'expected_price') + def check_percentage(self): + for record in self: + if record.selling_price and record.expected_price: + if ( + float_compare( + record.selling_price, + record.expected_price * 0.9, + precision_digits=1, + ) + < 0 + ): + raise ValidationError( + 'the selling perice cant be less than 90% of the expected price' + ) + + @api.ondelete(at_uninstall=False) + def _unlink_if_user_inactive(self): + for record in self: + if record.state not in ['cancelled', 'new']: + raise UserError( + 'cannot delete - only delete from the state `new` and `cancelled`' + ) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..746b6782286 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,158 @@ +from odoo import fields, models, api +from odoo.exceptions import UserError + + +class EstatePropertyOffer(models.Model): + _name = 'estate.property.offer' + _description = 'estate property offer' + _check_positive_offer_price = models.Constraint( + 'CHECK (price >= 0)', 'price should be positive' + ) + _order = 'price desc' + + price = fields.Float(copy=False) + status = fields.Selection( + copy=False, + string='status', + selection=[('accepted', "Accepted"), ('refused', "Refused")], + ) + partner_id = fields.Many2one( + 'res.partner', required=True, default=lambda self: self.env.user.partner_id.id + ) + property_id = fields.Many2one('estate.property', required=True) + validity = fields.Integer(default=7, copy=False) + deadline = fields.Date( + compute='_compute_deadline', store=True, inverse='_inverse_deadline' + ) + + property_type_id = fields.Many2one( + 'estate.property.type', related='property_id.property_type_id', store=True + ) + + @api.depends('validity', 'deadline') + def _compute_deadline(self): + for record in self: + record.deadline = fields.Date.add(fields.Date.today(), days=record.validity) + + def _inverse_deadline(self): + for record in self: + today_date = fields.Date.today() + record.validity = (record.deadline - today_date).days + + def action_accept_offer(self): + for offer in self: + for offer.property_id in offer.property_id: + if ( + offer.property_id.state == 'offer accepted' + or offer.property_id.state == 'sold' + ): + raise UserError( + 'An offer has already been accepted for this property.' + ) + else: + offer.status = 'accepted' + offer.property_id.state = 'offer accepted' + offer.property_id.buyer_id = offer.partner_id + offer.property_id.selling_price = offer.price + + won_stage_id = self.env['crm.stage'].search( + [('name', 'in', ['Won'])] + ) + self.env['crm.lead'].create( + { + 'create_date': fields.Date, + 'display_name': 'test', + 'expected_revenue': self.price, + 'name': 'test', + 'type': 'opportunity', + 'won_status': 'won', + 'probability': 100, + 'stage_id': won_stage_id.id, + } + ) + self.env.cr.commit() + + for offers in self.property_id.offer_ids: + if offers != self: + offers.status = 'refused' + + lead = self.env['crm.lead'].create( + { + 'name': 'test123', + 'expected_revenue': self.price, + 'won_status': 'lost', + 'create_date': fields.Date, + 'display_name': 'test', + 'name': 'test', + 'type': 'opportunity', + 'won_status': 'lost', + 'probability': 0, + } + ) + lead.action_set_lost() + self.env.cr.commit() + + def action_reject_offer(self): + for offer in self: + if offer.status == 'accepted': + if offer.property_id.state == 'sold': + raise UserError( + 'the property is sold. CANT REJECT THE OFFER - THANK YOU' + ) + else: + offer.status = 'refused' + offer.property_id.state = 'offer received' + offer.property_id.buyer_id = False + offer.property_id.selling_price = 0 + + elif offer.status == 'refused': + raise UserError('This offer has already been refused.') + else: + offer.status = 'refused' + + lead = self.env['crm.lead'].create( + { + 'name': 'test123', + 'won_status': 'lost', + 'expected_revenue': self.price, + 'create_date': fields.Date, + 'display_name': 'test', + 'name': 'test', + 'type': 'opportunity', + 'won_status': 'won', + 'probability': 0, + } + ) + lead.action_set_lost() + self.env.cr.commit() + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + property_id = self.env['estate.property'].browse(vals.get('property_id')) + if property_id.offer_ids: + max_offer_price = max(property_id.offer_ids.mapped('price')) + if vals.get('price', 0) <= max_offer_price: + raise UserError( + 'The offer price must be higher than the current highest offer (%s).' + % max_offer_price + ) + property_id.state = 'offer received' + return super().create(vals_list) + + @api.model + def _cron_expired_offer(self): + ''' + Cron job to set expired property offers to 'refused' status. + ''' + print('cronran') + today = fields.Date.today() + offers_expired = self.search( + [('deadline', '<', today), ('status', '!=', 'refused')] + ) + if offers_expired: + offers_expired.write({'status': 'refused'}) + self.env.cr.commit() + print('offers set to refused by cron job') + else: + print('cron job ran : no offers exceeding the deadline') diff --git a/estate/models/estate_property_offer_wizard.py b/estate/models/estate_property_offer_wizard.py new file mode 100644 index 00000000000..e9fbb5297d3 --- /dev/null +++ b/estate/models/estate_property_offer_wizard.py @@ -0,0 +1,38 @@ +from odoo import fields, models, api + + +class EstatePropertyOfferWizard(models.TransientModel): + _name = 'estate.property.offer.wizard' + _description = 'estate.property.offer.wizard model for the wizard' + + price = fields.Float(string='Offer Price', required=True) + + partner_id = fields.Many2one('res.partner', string='Buyer', required=True) + + def action_create_offers(self): + """ + + properties that are not Sold or Cancelled thn also creates an offer for each + + """ + + self.ensure_one() + + new_offer = [] + eligible_property = self.env['estate.property'].search( + [('state', 'not in', ['sold', 'cancelled', 'offer accepted'])] + ) + + for prop in eligible_property: + new_offer.append( + { + 'price': self.price, + 'partner_id': self.partner_id.id, + 'property_id': prop.id, + } + ) + + if new_offer: + self.env['estate.property.offer'].create(new_offer) + + return {'type': 'ir.actions.act_window_close'} diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..3c9d5fe31cb --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,11 @@ +from odoo import fields, models + + +class EstatePropertytTag(models.Model): + _name = 'estate.property.tag' + _description = 'Estate_property_tag' + _unique_tag = models.UniqueIndex('(name)', 'The name of the tag must be unique') + _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..faa784b7ed1 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,21 @@ +from odoo import fields, models, api + + +class EstatePropertyType(models.Model): + _name = 'estate.property.type' + _description = 'Estate_property_type' + _unique_type = models.UniqueIndex('(name)', 'property type should be unique') + _order = 'sequence' + + name = fields.Char(required=True) + sequence = fields.Integer( + default=1, help='Used to order the property types. Lower is better.' + ) + offer_ids = fields.One2many('estate.property.offer', 'property_type_id') + offer_count = fields.Integer(default=0, compute='_compute_total_count', store=True) + property_ids = fields.One2many('estate.property', 'property_type_id') + + @api.depends('offer_ids') + def _compute_total_count(self): + for rec in self: + rec.offer_count = len(rec.offer_ids.mapped('id')) diff --git a/estate/models/estate_property_visit.py b/estate/models/estate_property_visit.py new file mode 100644 index 00000000000..07ac4046892 --- /dev/null +++ b/estate/models/estate_property_visit.py @@ -0,0 +1,35 @@ +from odoo import api, fields, models +from odoo.exceptions import ValidationError + + +class EstatePropertyVisit(models.Model): + _name = 'estate.property.visit' + _description = 'estate property visit' + _order = 'date desc' + _sql_const = models.UniqueIndex( + '(date, property_id)', + 'This property is already booked for a visit on this date!', + ) + + property_id = fields.Many2one('estate.property', required=True) + visitor_id = fields.Many2one('res.partner', required=True) + date = fields.Date(required=True) + comment = fields.Text() + state = fields.Selection( + string='status', + selection=[ + ('new', "New"), + ('scheduled', "Scheduled"), + ('done', "Done"), + ('cancelled', "Cancelled"), + ], + default='new', + ) + + @api.constrains('date') + def _compute_date_clash(self): + for rec in self: + if rec.date < fields.Date.today(): + raise ValidationError('The visit date cannot be in the past.') + if rec.date: + rec.state = 'scheduled' diff --git a/estate/models/inherited.py b/estate/models/inherited.py new file mode 100644 index 00000000000..1d11733292b --- /dev/null +++ b/estate/models/inherited.py @@ -0,0 +1,7 @@ +from odoo import fields, models + + +class Inherited(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..2f3aa025e20 --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,26 @@ +id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink +access_estate_property_admin,estate_property_admin,model_estate_property,estate.property_admin,1,1,1,1 +access_estate_maintainance_request,access_estate_maintainance_request,model_estate_maintainance_request,estate.property_admin,1,1,1,1 +access_estate_property_offer,access_estate_property_offer,model_estate_property_offer,estate.property_admin,1,1,1,1 +access_estate_property_visit,access_estate_property_visit,model_estate_property_visit,estate.property_admin,1,1,1,1 +access_estate_property_type,access_estate_property_type,model_estate_property_type,estate.property_admin,1,1,1,1 +access_estate_property_tag,access_estate_property_tag,model_estate_property_tag,estate.property_admin,1,1,1,1 +access_estate_property_agent,estate_property_agent,model_estate_property,estate.property_agent,1,1,1,0 +access_estate_property_offer_agent,estate_property_offer_agent,model_estate_property_offer,estate.property_agent,1,1,1,0 +access_estate_maintainance_request_agent,access_estate_maintainance_request_agent,model_estate_maintainance_request,estate.property_agent,1,0,0,0 +access_estate_property_visit_agent,access_estate_property_visit_agent,model_estate_property_visit,estate.property_agent,1,0,0,0 +access_estate_property_type_agent,access_estate_property_type_agent,model_estate_property_type,estate.property_agent,1,1,1,0 +access_estate_property_tag_agent,access_estate_property_tag_agent,model_estate_property_tag,estate.property_agent,1,1,1,0 +access_estate_property_manager,estate_property_manager,model_estate_property,estate.property_manager,1,1,1,1 +access_estate_property_offer_manager,estate_property_offer_manager,model_estate_property_offer,estate.property_manager,1,1,1,1 +access_estate_maintainance_request_manager,access_estate_maintainance_request_manager,model_estate_maintainance_request,estate.property_manager,1,0,0,0 +access_estate_property_visit_manager,access_estate_property_visit_manager,model_estate_property_visit,estate.property_manager,1,1,1,1 +access_estate_property_type_manager,access_estate_property_type_manager,model_estate_property_type,estate.property_manager,1,1,1,1 +access_estate_property_tag_manager,access_estate_property_tag_manager,model_estate_property_tag,estate.property_manager,1,1,1,1 +access_estate_property_maintainance_staff,estate_property_maintainance_staff,model_estate_property,estate.property_maintainance_staff,1,1,0,0 +access_estate_property_offer_maintainance_staff,estate_property_offer_maintainance_staff,model_estate_property_offer,estate.property_maintainance_staff,1,0,0,0 +access_estate_maintainance_request_maintainance_staff,access_estate_maintainance_request_maintainance_staff,model_estate_maintainance_request,estate.property_maintainance_staff,1,1,1,1 +access_estate_property_visit_maintainance_staff,access_estate_property_visit_maintainance_staff,model_estate_property_visit,estate.property_maintainance_staff,1,0,0,0 +access_estate_property_type_maintainance_staff,access_estate_property_type_maintainance_staff,model_estate_property_type,estate.property_maintainance_staff,1,0,0,0 +access_estate_property_tag_maintainance_staff,access_estate_property_tag_maintainance_staff,model_estate_property_tag,estate.property_maintainance_staff,1,0,0,0 +access_estate_property_offer_wizard_all,access_estate_property_offer_wizard_all,model_estate_property_offer_wizard,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/estate/security/security_groups.xml b/estate/security/security_groups.xml new file mode 100644 index 00000000000..3484a3e8a8b --- /dev/null +++ b/estate/security/security_groups.xml @@ -0,0 +1,27 @@ + + + + Real Estate + + + Admin + + + + + + Agent + + + + + Manager + + + + + Maintainance Staff + + + + diff --git a/estate/static/icon/icon.jpeg b/estate/static/icon/icon.jpeg new file mode 100644 index 00000000000..bad92496397 Binary files /dev/null and b/estate/static/icon/icon.jpeg differ diff --git a/estate/views/estate_maintainance_form.xml b/estate/views/estate_maintainance_form.xml new file mode 100644 index 00000000000..6ffe1a5b44d --- /dev/null +++ b/estate/views/estate_maintainance_form.xml @@ -0,0 +1,42 @@ + + + + estate_maintainance_action + estate.maintainance.request + list,form + + + estate.maintainance.type.list.view + estate.maintainance.request + + + + + + + + + + + + + + estate.maintainance.type.form.view + estate.maintainance.request + +
+ + + + + + + + + + + +
+
+
+
\ No newline at end of file diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..457f2173fbd --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/estate/views/estate_propert_offer_wizard.xml b/estate/views/estate_propert_offer_wizard.xml new file mode 100644 index 00000000000..b1c3e9b4f05 --- /dev/null +++ b/estate/views/estate_propert_offer_wizard.xml @@ -0,0 +1,26 @@ + + + + Launch the Wizard + estate.property.offer.wizard + form + new + + + estate_property_offer_form_wizard + estate.property.offer.wizard + +
+ + + + +
+ +
+
+
+
+
+
+
\ No newline at end of file diff --git a/estate/views/estate_property_inherited.xml b/estate/views/estate_property_inherited.xml new file mode 100644 index 00000000000..3b1f8af125d --- /dev/null +++ b/estate/views/estate_property_inherited.xml @@ -0,0 +1,13 @@ + + + + inherited.model.form.inherit + res.users + + + + + + + + \ No newline at end of file diff --git a/estate/views/estate_property_offer_view.xml b/estate/views/estate_property_offer_view.xml new file mode 100644 index 00000000000..ad2bf4a95a5 --- /dev/null +++ b/estate/views/estate_property_offer_view.xml @@ -0,0 +1,54 @@ + + + + estate_property_offer + estate.property.offer + list,form + + + offers + estate.property.offer + list,form + [('property_type_id', '=', active_id)] + + + estate.property.offer.list.view + estate.property.offer + + + + + + + + + + + estate.property.offer.form.view + estate.property.offer + +
+ + + + + + + + + +
+
+
+ + expire_offer + + code + model._cron_expired_offer() + + 1 + minutes + + + +
\ No newline at end of file diff --git a/estate/views/estate_property_tag_view.xml b/estate/views/estate_property_tag_view.xml new file mode 100644 index 00000000000..1fa40197226 --- /dev/null +++ b/estate/views/estate_property_tag_view.xml @@ -0,0 +1,30 @@ + + + + estate_property_tag + estate.property.tag + list,form + + + estate.property.tag.list.view + estate.property.tag + + + + + + + + estate.property.tag.form.view + estate.property.tag + +
+ + + + + +
+
+
+
\ No newline at end of file diff --git a/estate/views/estate_property_type_view.xml b/estate/views/estate_property_type_view.xml new file mode 100644 index 00000000000..420c9f7b4d6 --- /dev/null +++ b/estate/views/estate_property_type_view.xml @@ -0,0 +1,51 @@ + + + + estate_property_type_action + estate.property.type + list,form + + + estate.property.type.tree.view + estate.property.type + + + + + + + + + + estate.property.type.form.view + estate.property.type + +
+ + +
+ +
+ + + + + + + + +
+
+
+
+
+
\ No newline at end of file diff --git a/estate/views/estate_property_view.xml b/estate/views/estate_property_view.xml new file mode 100644 index 00000000000..eab1bc60dff --- /dev/null +++ b/estate/views/estate_property_view.xml @@ -0,0 +1,177 @@ + + + + Real-estate Property"s + estate.property + {'search_default_available': 1} + kanban,list,form + + + estate.property.list.view + estate.property + + +
+
+ + + + + + + + +
+
+
+ + estate.property.form.view + estate.property + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +