diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 00000000000..105ce2da2d6 --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000000..5c1c8885418 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/tutorials.iml b/.idea/tutorials.iml new file mode 100644 index 00000000000..d0876a78d06 --- /dev/null +++ b/.idea/tutorials.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js deleted file mode 100644 index 637fa4bb972..00000000000 --- a/awesome_dashboard/static/src/dashboard.js +++ /dev/null @@ -1,10 +0,0 @@ -/** @odoo-module **/ - -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/.dashboard.js.swp b/awesome_dashboard/static/src/dashboard/.dashboard.js.swp new file mode 100644 index 00000000000..939b964c0cc Binary files /dev/null and b/awesome_dashboard/static/src/dashboard/.dashboard.js.swp differ diff --git a/awesome_dashboard/static/src/dashboard/.dashboard.xml.swp b/awesome_dashboard/static/src/dashboard/.dashboard.xml.swp new file mode 100644 index 00000000000..64c966ad3b9 Binary files /dev/null and b/awesome_dashboard/static/src/dashboard/.dashboard.xml.swp differ diff --git a/awesome_dashboard/static/src/dashboard/.liste.js.swp b/awesome_dashboard/static/src/dashboard/.liste.js.swp new file mode 100644 index 00000000000..5b5eb366245 Binary files /dev/null and b/awesome_dashboard/static/src/dashboard/.liste.js.swp differ diff --git a/awesome_dashboard/static/src/dashboard/.stats_service.js.swp b/awesome_dashboard/static/src/dashboard/.stats_service.js.swp new file mode 100644 index 00000000000..502785453df Binary files /dev/null and b/awesome_dashboard/static/src/dashboard/.stats_service.js.swp differ diff --git a/awesome_dashboard/static/src/dashboard/card/.card.js.swp b/awesome_dashboard/static/src/dashboard/card/.card.js.swp new file mode 100644 index 00000000000..1b20fc453fa Binary files /dev/null and b/awesome_dashboard/static/src/dashboard/card/.card.js.swp differ diff --git a/awesome_dashboard/static/src/dashboard/card/.card.xml.swp b/awesome_dashboard/static/src/dashboard/card/.card.xml.swp new file mode 100644 index 00000000000..882b409f3ac Binary files /dev/null and b/awesome_dashboard/static/src/dashboard/card/.card.xml.swp differ diff --git a/awesome_dashboard/static/src/dashboard/card/card.js b/awesome_dashboard/static/src/dashboard/card/card.js new file mode 100644 index 00000000000..7d7e9ee858f --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/card/card.js @@ -0,0 +1,15 @@ +import { Component } from "@odoo/owl"; + +export class Card extends Component { + static template = 'card.card'; + static props = { + size: {type: 'Number', optional: 'true',}, + slots: {type: 'Object', optional: 'true',}, + debug: {type: 'Function', optional: 'true',}, + } + + setup() { + this.size = this.props.size ? this.props.size : 1; + if (this.props.debug) { this.props.debug(this); } + } +} diff --git a/awesome_dashboard/static/src/dashboard/card/card.xml b/awesome_dashboard/static/src/dashboard/card/card.xml new file mode 100644 index 00000000000..6851c96f4c1 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/card/card.xml @@ -0,0 +1,10 @@ + + + +
+ +
+ +
+ +
diff --git a/awesome_dashboard/static/src/dashboard/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js new file mode 100644 index 00000000000..138a50ba13f --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -0,0 +1,88 @@ +/** @odoo-module **/ + +import { Component, onWillStart, onWillUnmount, useState, onWillRender } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { Layout } from "@web/search/layout"; +import { useService } from "@web/core/utils/hooks"; +import { Card } from "./card/card"; +import { PieCard } from "./pie_card/pie_card"; +import { NumberCard } from "./number_card/number_card"; +import { PieChart } from "./piechart/piechart"; + +class AwesomeDashboard extends Component { + static template = "awesome_dashboard.AwesomeDashboard"; + static components = { Layout, Card, PieCard, NumberCard, PieChart }; + + setup() { + this.testData = registry.category('awesome_dashboard.data').get('graphs').map((elt) => JSON.stringify(elt)); + + this.state = useState({numbers: null, graphs: null, data: null, happygary: 'ᕦ( ᐛ )ᕡ'}); + + this.onStatsUpdate = (vals) => { + this.state.data = vals; + } + + this.dance = () => { + this.state.happygary = this.state.happygary == 'ᕦ( ᐛ )ᕡ' ? 'ᕕ( ᐕ )ᕗ' : 'ᕦ( ᐛ )ᕡ' + setTimeout(() => {this.dance();}, 700) + } + + this.dance(); + + this.action = useService('action'); + + this.statsService = useService('awesome_dashboard.stats'); + + + onWillStart(async () => { + this.statsService.setActive(this.onStatsUpdate); + this.state.data = await this.statsService.getValues(); + }); + + onWillUnmount(() => {this.statsService.clearActive()}); + + onWillRender(() => { + const numbers = registry.category('awesome_dashboard.data').get('numbers'); + const directValues = numbers.filter((elt) => elt.source == null); + const loadedValues = numbers.filter((elt) => elt.source).map((elt) => { + elt.value = this.state.data[elt.source]; + return elt; + }); + + this.state.numbers = [ + ...loadedValues, + ...directValues, + ].map((elt) => JSON.stringify(elt)); + + const graphs = registry.category('awesome_dashboard.data').get('graphs'); + const directCharts = graphs.filter((elt) => elt.source == null); + const loadedCharts = graphs.filter((elt) => elt.source).map((elt) => { + elt.data = this.state.data[elt.source]; + return elt; + }); + + this.state.graphs = [ + ...loadedCharts, + ...directCharts, + ].map((elt) => JSON.stringify(elt)); + }); + + } + + customersButton() { + this.action.doAction('base.action_partner_form'); + } + + leadsButton() { + this.action.doAction({ + type: 'ir.actions.act_window', + name: 'Leads', + target: 'current', + res_model: 'crm.lead', + views: [[false, 'list'], [false, 'form']], + }); + } + +} + +registry.category("lazy_components").add("awesome_dashboard.dashboard", 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..5440a470a28 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.scss @@ -0,0 +1,3 @@ +.o_dashboard { + background-color: deepskyblue; +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml new file mode 100644 index 00000000000..cab57ef6de3 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -0,0 +1,17 @@ + + + + + + +

+ + + + + + +
+
+ +
diff --git a/awesome_dashboard/static/src/dashboard/liste.js b/awesome_dashboard/static/src/dashboard/liste.js new file mode 100644 index 00000000000..9d25459ed1a --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/liste.js @@ -0,0 +1,63 @@ +import { registry } from "@web/core/registry"; + + +const numbers = [ + { + title: 'Some title.', + value: 103, + }, + { + title: 'Some other title.', + value: 418, + }, + { + title: 'Average Quantity', + source: 'average_quantity', + }, + { + title: 'Average Time', + source: 'average_time', + }, + { + title: 'Number of Cancelled Orders', + source: 'nb_cancelled_orders', + }, + { + title: 'Number of New Orders', + source: 'nb_new_orders', + }, + { + title: 'Total Amount', + source: 'total_amount', + }, +]; + +registry.category('awesome_dashboard.data').add('numbers', numbers); + +const graphs = [ + { + id: 'graph1', + title: 'An graph.', + data: { + value1: 9, + value2: 3, + value3: 9, + }, + }, + { + id: 'graph2', + title: 'Another graph.', + data: { + 'Jean-Eud le Tacos Vegan': 1536, + 'Bérénice la Saucisse Créatrice': 32, + 'Monsieur Puel Monsieur': 3, + }, + }, + { + id: 'shirt_size_pie', + title: 'T-Shirt Sales by Size', + source: 'orders_by_size', + }, +]; + +registry.category('awesome_dashboard.data').add('graphs', graphs); diff --git a/awesome_dashboard/static/src/dashboard/number_card/.number_card.js.swp b/awesome_dashboard/static/src/dashboard/number_card/.number_card.js.swp new file mode 100644 index 00000000000..54c30b08d54 Binary files /dev/null and b/awesome_dashboard/static/src/dashboard/number_card/.number_card.js.swp differ diff --git a/awesome_dashboard/static/src/dashboard/number_card/.number_card.xml.swp b/awesome_dashboard/static/src/dashboard/number_card/.number_card.xml.swp new file mode 100644 index 00000000000..70c961054c8 Binary files /dev/null and b/awesome_dashboard/static/src/dashboard/number_card/.number_card.xml.swp differ diff --git a/awesome_dashboard/static/src/dashboard/number_card/number_card.js b/awesome_dashboard/static/src/dashboard/number_card/number_card.js new file mode 100644 index 00000000000..6833f774c5b --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/number_card/number_card.js @@ -0,0 +1,27 @@ +import { Component } from "@odoo/owl"; +import { Card } from "../card/card"; + + +export class NumberCard extends Component { + static template = 'number_card.number_card'; + static components = { Card }; + static props = { + title: {type: 'String', optional: 'true',}, + value: {type: 'Number', optional: 'true',}, + size: {type: 'Number', optional: 'true',}, + stringProps: {type: 'String', optional: 'true',}, + + } + setup() { + if (this.props.stringProps) { + this.stringProps = JSON.parse(this.props.stringProps); + this.title = this.stringProps.title ? this.stringProps.title : 'No Title.'; + this.value = this.stringProps.value ? this.stringProps.value : 0; + this.size = this.stringProps.size; + } else { + this.title = this.props.title ? this.props.title : 'No Title.'; + this.value = this.props.value ? this.props.value : 0; + this.size = this.props.size; + } + } +} diff --git a/awesome_dashboard/static/src/dashboard/number_card/number_card.xml b/awesome_dashboard/static/src/dashboard/number_card/number_card.xml new file mode 100644 index 00000000000..c91a38c7d28 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/number_card/number_card.xml @@ -0,0 +1,10 @@ + + + + +

+

+
+
+ +
diff --git a/awesome_dashboard/static/src/dashboard/pie_card/.pie_card.js.swp b/awesome_dashboard/static/src/dashboard/pie_card/.pie_card.js.swp new file mode 100644 index 00000000000..f99ad83afb7 Binary files /dev/null and b/awesome_dashboard/static/src/dashboard/pie_card/.pie_card.js.swp differ diff --git a/awesome_dashboard/static/src/dashboard/pie_card/.pie_card.xml.swp b/awesome_dashboard/static/src/dashboard/pie_card/.pie_card.xml.swp new file mode 100644 index 00000000000..4b8abe91fa3 Binary files /dev/null and b/awesome_dashboard/static/src/dashboard/pie_card/.pie_card.xml.swp differ diff --git a/awesome_dashboard/static/src/dashboard/pie_card/pie_card.js b/awesome_dashboard/static/src/dashboard/pie_card/pie_card.js new file mode 100644 index 00000000000..aa25f99c89f --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_card/pie_card.js @@ -0,0 +1,102 @@ +import { Component, onMounted, useState, onWillRender, markup } from "@odoo/owl"; +import { loadJS } from "@web/core/assets"; +import { Card } from "../card/card"; + +export class PieCard extends Component { + static template = 'pie_card.pie_card'; + static components = { Card }; + static props = { + title: {type: 'String', optional: 'true',}, + data: {type: 'Object', optional: 'true',}, + size: {type: 'Number', optional: 'true',}, + id: {type: 'String', optional: 'true',}, + stringProps: {type: 'String', optional: 'true',}, + }; + + setup() { + if (this.props.stringProps) { + this.stringProps = JSON.parse(this.props.stringProps); + + this.id = this.stringProps.id ? this.stringProps.id : 'piechart' + + this.data = this.stringProps.data ? this.stringProps.data : {'no data.': 1}; + + this.size = this.stringProps.size; + + this.title = this.stringProps.title; + } else { + this.id = this.props.id ? this.props.id : 'piechart' + + this.data = this.props.data ? this.props.data : {'no data.': 1}; + + this.size = this.props.size; + + this.title = this.props.title; + } + + + + + this.state = useState({data: null}); + + this.chart = null; + + + this.canvas = markup(` + + + `); + this.render = async () => { + + this.context = document.getElementById(this.id); + if (this.context == null || this.context == undefined) { + return; + } + + + const chartJS = await loadJS("/web/static/lib/Chart/Chart.js"); + + const config = { + type: 'pie', + data: { + labels: Object.keys(this.data), + datasets: [ + { + label: 'ds1', + data: Object.values(this.data), + } + ], + }, + options: { + responsive: true, + }, + }; + + const tempChart = Chart.getChart(this.context); + if (tempChart) tempChart.destroy(); + + this.chart = new Chart(this.context, config); + + } + + + onWillRender(() => { + this.render(); + }); + + onMounted(() => { + this.render(); + /* + // epilepsy mode + this.context.onmousemove = () => { + const hex = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'] + const genHex = () => `${hex[Math.floor(Math.random() * 16)]}${hex[Math.floor(Math.random() * 16)]}` + const genColor = () => `#${genHex()}${genHex()}${genHex()}` + this.context.style.backgroundColor = genColor(); + } + */ + }); + + + } +} diff --git a/awesome_dashboard/static/src/dashboard/pie_card/pie_card.xml b/awesome_dashboard/static/src/dashboard/pie_card/pie_card.xml new file mode 100644 index 00000000000..45654bd3ada --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_card/pie_card.xml @@ -0,0 +1,10 @@ + + + + +

+ +
+
+ +
diff --git a/awesome_dashboard/static/src/dashboard/piechart/piechart.js b/awesome_dashboard/static/src/dashboard/piechart/piechart.js new file mode 100644 index 00000000000..0c1128f0211 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/piechart/piechart.js @@ -0,0 +1,59 @@ +import { Component, onMounted, useState, onWillRender } from "@odoo/owl"; +import { loadJS } from "@web/core/assets"; + +export class PieChart extends Component { + static template = 'piechart.piechart'; + static props = { + data: {type: 'Object',}, + }; + + setup() { + this.state = useState({data: null}); + + this.chart = null; + + this.render = async () => { + + this.context = document.getElementById('piechart'); + if (this.context == null || this.context == undefined) { + return; + } + + if (this.chart) { + this.chart.destroy(); + } + + const chartJS = await loadJS("/web/static/lib/Chart/Chart.js"); + + const config = { + type: 'pie', + data: { + labels: Object.keys(this.props.data), + datasets: [ + { + label: 'ds1', + data: Object.values(this.props.data), + } + ], + }, + options: { + responsive: true, + }, + }; + + + this.chart = new Chart(this.context, config); + + } + + onWillRender(() => { + this.render(); + }); + + onMounted(() => { + this.render(); + }); + + + } +} diff --git a/awesome_dashboard/static/src/dashboard/piechart/piechart.xml b/awesome_dashboard/static/src/dashboard/piechart/piechart.xml new file mode 100644 index 00000000000..e7a85e48ad7 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/piechart/piechart.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/awesome_dashboard/static/src/dashboard/stats_service.js b/awesome_dashboard/static/src/dashboard/stats_service.js new file mode 100644 index 00000000000..65debdaf182 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/stats_service.js @@ -0,0 +1,45 @@ +import { reactive } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { rpc } from "@web/core/network/rpc"; +import { memoize } from "@web/core/utils/functions"; + +const cache = reactive({values: null}, () => { + if (callback) { + callback(cache.values); + } + +}); +let isActive = true; +let callback = null; + +const nextReload = () => { + setTimeout(async () => { + + if (isActive) { + nextReload(); + cache.values = await rpc('/awesome_dashboard/statistics'); + } + + }, 5000); +} + +const setActive = (cb) => {isActive = true;nextReload();callback = cb;}; +const clearActive = () => {isActive = false;callback = null}; + +const test = () => {return [['test', 2], ['test2', 3]];} + +const getValues = async () => { + if (cache.values == null) { + cache.values = await rpc('/awesome_dashboard/statistics'); + } + + return cache.values; +}; + +export const StatsService = { + start() { + return { test, getValues, setActive, clearActive }; + } +} + +registry.category("services").add("awesome_dashboard.stats", StatsService); diff --git a/awesome_dashboard/static/src/dashboard_loader.js b/awesome_dashboard/static/src/dashboard_loader.js new file mode 100644 index 00000000000..0ed60d7cb1c --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_loader.js @@ -0,0 +1,16 @@ +import { Component } from "@odoo/owl"; +import { LazyComponent } from "@web/core/assets"; +import { registry } from "@web/core/registry"; +import { Dashboard } from "./dashboard/dashboard"; +import { PieChart } from "./dashboard/piechart/piechart"; + +export class DashboardLoader extends Component { + static template = 'dashboard_loader.dashboard_loader'; + static components = { Dashboard, LazyComponent } + setup() { + this.thing = 'awesome_dashboard.dashboard' + } +} + + +registry.category('actions').add('awesome_dashboard.dashboard', DashboardLoader) diff --git a/awesome_dashboard/static/src/dashboard_loader.xml b/awesome_dashboard/static/src/dashboard_loader.xml new file mode 100644 index 00000000000..1cc8a2ad101 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_loader.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js new file mode 100644 index 00000000000..413f2886b6c --- /dev/null +++ b/awesome_owl/static/src/card/card.js @@ -0,0 +1,34 @@ +import { Component, useState } from '@odoo/owl'; + +export class Card extends Component { + static template = 'card.card'; + static props = { + title: { + type: 'String', + optional: 'true', + }, + ace: { + type: 'Function', + optional: 'true', + }, + slots: { + type: 'Object', + optional: 'true', + }, + }; + + setup() { + this.state = useState({collapsed: true, text: "expand"}); + this.title = this.props.title ? this.props.title : 'No Title'; + if (this.props.ace) { + this.props.ace(this); + } + } + + click() { + this.state.collapsed = !this.state.collapsed; + this.state.text = this.state.collapsed?'expand':'collapse'; + } + + +} diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml new file mode 100644 index 00000000000..abc51a4101f --- /dev/null +++ b/awesome_owl/static/src/card/card.xml @@ -0,0 +1,12 @@ + + + +
+
+ + + +
+
+ +
diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js new file mode 100644 index 00000000000..bb422e38fb7 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.js @@ -0,0 +1,16 @@ +import { Component, useState } from "@odoo/owl"; + +export class Counter extends Component { + static template = 'counter.counter'; + static props = { + onChange: {type: 'Function', optional: 'true',}, + } + setup() { + this.counter = useState({value: 0}); + } + + increment() { + this.counter.value++; + this.props.onChange('Prout!'); + } +} diff --git a/awesome_owl/static/src/counter/counter.xml b/awesome_owl/static/src/counter/counter.xml new file mode 100644 index 00000000000..34135a709f4 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.xml @@ -0,0 +1,11 @@ + + + + +
+

COUNTER :

+ +
+
+ +
diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 657fb8b07bb..e287f7cc380 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,7 +1,21 @@ /** @odoo-module **/ -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"; + static template = "awesome_owl.playground"; + static components = { Counter, Card, TodoList }; + static props = {}; + + setup() { + this.counter = useState({value: 0}); + } + + + updateTotal(str) { + this.counter.value++; + } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..b86252a4d20 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -1,10 +1,57 @@ - -
- hello world -
-
+ + +
+ +

I told you it was really cool!

+
+ + + + + +

Total :

+
+ + + + +
+ +
+
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..76f3335b3e9 --- /dev/null +++ b/awesome_owl/static/src/todo_item/todo_item.js @@ -0,0 +1,31 @@ +import { Component, useState } from '@odoo/owl'; + +export class TodoItem extends Component { + static template = 'todo_item.todo_item'; + static props = { + id: {type: 'Number',}, + title: {type: 'String', optional: 'true',}, + description: {type: 'String', optional: 'true',}, + isCompleted: {type: 'Boolean', optional: 'true',}, + update: {type: 'Function',}, + remove: {type: 'Function',}, + } + setup() { + this.contents = useState({ + id: this.props.id, + title: this.props.title ? this.props.title : 'No Title.', + description: this.props.description ? this.props.description : 'No Description.', + isCompleted: this.props.isCompleted, + }); + + } + + click() { + this.contents.isCompleted = !this.contents.isCompleted; + this.props.update(this.contents); + } + + rubbish_bin() { + this.props.remove(this.contents); + } +} 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..150d09e4ffc --- /dev/null +++ b/awesome_owl/static/src/todo_item/todo_item.xml @@ -0,0 +1,12 @@ + + + + +
+
+

+ +
+
+ +
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..e7f31340816 --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_list.js @@ -0,0 +1,59 @@ +import { Component, useState, markup } from '@odoo/owl'; +import { TodoItem } from '../todo_item/todo_item' + +export class TodoList extends Component { + static template = 'todo_list.todo_list'; + static props = {}; + static components = {TodoItem}; + + setup() { + this.list = useState({items: []}); + + this.list.items = JSON.parse(window.localStorage.getItem('todo')); + } + + update(item) { + for (let i = 0;i < this.list.items.length;i++) { + if (this.list.items[i].id == item.id) { + this.list.items[i].title = item.title; + this.list.items[i].description = item.description; + this.list.items[i].isCompleted = item.isCompleted; + } + } + + window.localStorage.setItem('todo', JSON.stringify(this.list.items)); + } + + create_task(e) { + if (e.keyCode != 13) return; + let newTask = { + id: 0, + title: document.getElementById('title').value, + description: document.getElementById('description').value, + isCompleted: false, + } + + if (newTask.title == '' || newTask.description == '') return; + + for (let i = 0;i < this.list.items.length;i++) { + if (this.list.items[i].id >= newTask.id) { + newTask.id = this.list.items[i].id; + } + } + newTask.id++; + this.list.items = [...this.list.items, newTask]; + window.localStorage.setItem('todo', JSON.stringify(this.list.items)); + document.getElementById('title').value = ''; + document.getElementById('description').value = ''; + } + + remove(item) { + const index = this.list.items.findIndex((elt) => elt.id == item.id); + if (index >= 0) { + this.list.items.splice(index, 1); + } + + + window.localStorage.setItem('todo', JSON.stringify(this.list.items)); + } +} 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..5cc812aa94e --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_list.xml @@ -0,0 +1,17 @@ + + + + +
+
+ + +
+ + + + +
+
+ +
diff --git a/estateaccountinator/__init__.py b/estateaccountinator/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/estateaccountinator/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/estateaccountinator/__manifest__.py b/estateaccountinator/__manifest__.py new file mode 100644 index 00000000000..2c2bf67f6d5 --- /dev/null +++ b/estateaccountinator/__manifest__.py @@ -0,0 +1,14 @@ +{ + 'name': 'estate-accountinator', + 'description': 'Inator that helps you do accounting on real estate', + 'category': 'Tutorials/Accountinator', + 'author': 'gato', + 'depends': [ + 'realestatinator', + 'account', + ], + 'installable': True, + 'application': True, + 'auto_install': False, + 'version': '0.1', +} diff --git a/estateaccountinator/models/__init__.py b/estateaccountinator/models/__init__.py new file mode 100644 index 00000000000..5e1963c9d2f --- /dev/null +++ b/estateaccountinator/models/__init__.py @@ -0,0 +1 @@ +from . import estate_property diff --git a/estateaccountinator/models/estate_property.py b/estateaccountinator/models/estate_property.py new file mode 100644 index 00000000000..75aa2e362dd --- /dev/null +++ b/estateaccountinator/models/estate_property.py @@ -0,0 +1,24 @@ +from odoo import api, Command, fields, models + + +class EstateProperty(models.Model): + _inherit = 'estate.property' + + def mark_sold(self): + for record in self: + line_defs = [ + { + 'name': f'selling price (6% of {record.selling_price})', + 'quantity': 1, + 'price_unit': 0.06*record.selling_price + }, + { + 'name': 'administrative fees', + 'quantity': 1, + 'price_unit': 100 + }, + ] + lines = [Command.create(line) for line in line_defs] + values = {'partner_id': record.buyer.id, 'move_type': 'out_invoice', 'line_ids': lines} + moves = self.env['account.move'].create(values) + return super().mark_sold() diff --git a/realestatinator/__init__.py b/realestatinator/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/realestatinator/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/realestatinator/__manifest__.py b/realestatinator/__manifest__.py new file mode 100644 index 00000000000..4beb62d3f2a --- /dev/null +++ b/realestatinator/__manifest__.py @@ -0,0 +1,22 @@ +{ + 'name': 'real-estate-inator', + 'description': 'Inator that helps you find real estate.', + 'category': 'Tutorials/RealEstateInator', + 'author': 'gato', + 'depends': [ + 'base', + 'web', + ], + 'data': [ + 'security/ir.model.access.csv', + 'views/estate_property_views.xml', + 'views/estate_property_type_views.xml', + 'views/estate_property_tags_views.xml', + 'views/estate_property_offer_views.xml', + 'views/estate_menus.xml', + ], + 'installable': True, + 'application': True, + 'auto_install': False, + 'version': '0.1', +} diff --git a/realestatinator/models/__init__.py b/realestatinator/models/__init__.py new file mode 100644 index 00000000000..a9bb2a2206a --- /dev/null +++ b/realestatinator/models/__init__.py @@ -0,0 +1,6 @@ +from . import estate_property +from . import estate_property_offer +from . import estate_property_tags +from . import estate_property_type +from . import res_partner +from . import res_users diff --git a/realestatinator/models/estate_property.py b/realestatinator/models/estate_property.py new file mode 100644 index 00000000000..8a5f656ce6a --- /dev/null +++ b/realestatinator/models/estate_property.py @@ -0,0 +1,111 @@ +from odoo import _, api, exceptions, fields, models + + +class EstatePropery(models.Model): + _name = 'estate.property' + _description = 'real estate property' + _order = 'id desc' + + + sequence = fields.Integer('Sequence', default=0) + name = fields.Char('Title', required=True) + description = fields.Text('Description') + postcode = fields.Char('Postcode') + date_availability = fields.Date('Available Date', copy=False, default=fields.Date.add(fields.Date.today(), months=+3)) + expected_price = fields.Float('Expected Price') + selling_price = fields.Float('Selling Price', readonly=True, copy=False) + bedrooms = fields.Integer('Bedrooms', default=2) + living_area = fields.Integer('Living Area') + facades = fields.Integer('Facades') + garage = fields.Boolean('Garage') + garden = fields.Boolean('Garden') + garden_area = fields.Integer('Garden Area') + garden_orientation = fields.Selection(string='Garden Orientation', selection=[ + ('north', 'North'), + ('south', 'South'), + ('east', 'East'), + ('west', 'West') + ]) + active = fields.Boolean('Active', default=False) + state = fields.Selection(string='State', selection=[ + ('new', 'New'), + ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('cancelled', 'Cancelled') + ], default='new') + property_type_id = fields.Many2one('estate.property.type', string='Property Type') + buyer = fields.Many2one('res.partner', string='Buyer', copy=False) + sales_person = fields.Many2one('res.users', string='Sales Person', default=lambda self: self.env.user) + tag_ids = fields.Many2many('estate.property.tags', string='Tags') + offer_ids = fields.One2many('estate.property.offer', 'property_id', string='Offer') + total_area = fields.Integer('Total Area', readonly=True, compute='_compute_total_area') + best_price = fields.Float('Best Offer', compute='_compute_best_price') + + + _sql_constraints = [ + ('check_expected_price_positive', 'CHECK (0 < expected_price)', 'Check that the expected price is strictly positive'), + + ] + + @api.depends('living_area', 'garden_area') + def _compute_total_area(self): + for line in self: + line.total_area = line.living_area + line.garden_area + + @api.depends('offer_ids.price') + def _compute_best_price(self): + for record in self: + prices = [0] + record.offer_ids.mapped('price') + record.best_price = max(prices) + + @api.onchange('garden') + def _set_garden_properties(self): + self.garden_orientation = 'north' if self.garden else '' + self.garden_area = 10 if self.garden else 0 + + # if record.garden: + # if record.garden_orientation not in ['north', 'east', 'west', 'south']: + # record.garden_orientation = 'north' + # if record.garden_area == 0: + # record.garden_area = 10 + # else: + # record.garden_orientation = '' + # record.garden_area = 0 + + def mark_cancelled(self): + for record in self: + if record.state == 'cancelled': + raise exceptions.UserError(_('This property is already cancelled.')) + + if record.state == 'sold': + raise exceptions.UserError(_('This property cannot be cancelled because it has already been sold.')) + + record.state = 'cancelled' + record.active = False + + def mark_sold(self): + for record in self: + if record.state == 'sold': + raise exceptions.UserError(_('This property is already sold.')) + + + if record.state == 'cancelled': + raise exceptions.UserError(_('This property cannot be sold because it has already been cancelled.')) + + record.state = 'sold' + record.active = False + + @api.constrains('selling_price') + def _check_selling_price(self): + for record in self: + if record.state not in ['offer_accepted', 'sold']: + return + if record.selling_price < 0.9 * record.expected_price: + raise exceptions.ValidationError(_('Selling price must be at least 90% of expected price.')) + + @api.ondelete(at_uninstall=False) + def _unlink(self): + for record in self: + if record.state not in ['new', 'cancelled']: + raise exceptions.UserError(_('Property must be either new or cancelled to be deleted.')) diff --git a/realestatinator/models/estate_property_offer.py b/realestatinator/models/estate_property_offer.py new file mode 100644 index 00000000000..9a5bb8688e4 --- /dev/null +++ b/realestatinator/models/estate_property_offer.py @@ -0,0 +1,67 @@ +from odoo import _, api, exceptions, fields, models + + +class EstatePropertyOffer(models.Model): + _name = 'estate.property.offer' + _description = 'estate property offer' + _order = 'price desc' + + + creation_date = fields.Date('Creation Date', default=fields.Date.today()) + price = fields.Float('Price') + status = fields.Selection(string='Status', selection=[ + ('accepted', 'Accepted'), + ('refused', 'Refused') + ], copy=False) + partner_id = fields.Many2one('res.partner', string='Partner') + property_id = fields.Many2one('estate.property', string='Property') + validity = fields.Integer('Validity', default=7, help='Number of days the offer is valid.') + date_deadline = fields.Date('Deadline', compute='_compute_deadline', inverse='_inverse_deadline') + property_type_id = fields.Many2one(related='property_id.property_type_id', store=True) + + + _sql_constraints = [ + ('check_offer_price_positive', 'CHECK (0 < price)', 'Check that the offer price is strictly positive.'), + ] + + + @api.depends('validity') + def _compute_deadline(self): + for record in self: + record.date_deadline = fields.Date.add(record.creation_date, days=record.validity) + + def _inverse_deadline(self): + for record in self: + delta = record.date_deadline - record.creation_date + record.validity = delta.days + + def refuse_offer(self): + for record in self: + if record.status == 'accepted': + record.property_id.state = 'offer_received' + record.property_id.selling_price = 0 + record.property_id.buyer = None + record.status = 'refused' + + def accept_offer(self): + for record in self: + if record.property_id.selling_price != 0: + raise exceptions.UserError(_('An offer as already been accepted for this property.')) + record.property_id.state = 'offer_accepted' + record.status = 'accepted' + record.property_id.selling_price = record.price + record.property_id.buyer = record.partner_id + + + @api.model_create_multi + def create(self, vals): + if "property_id" not in vals.keys(): + raise exceptions.UserError(f'A property must be provided for an offer.') + if "price" not in vals.keys(): + raise exceptions.UserError(f'A price must be provided for an offer.') + estate_property = self.env['estate.property'].browse(vals["property_id"]) + if vals["price"] < estate_property.best_price: + raise exceptions.UserError(f'Offer must be higher than the current best offer({estate_property.best_price})') + if estate_property.state == 'new': + estate_property.state = 'offer_received' + return super().create(vals) diff --git a/realestatinator/models/estate_property_tags.py b/realestatinator/models/estate_property_tags.py new file mode 100644 index 00000000000..559fb1e0c84 --- /dev/null +++ b/realestatinator/models/estate_property_tags.py @@ -0,0 +1,12 @@ +from odoo import fields, models + + +class EstatePropertyTags(models.Model): + _name = 'estate.property.tags' + _description = 'estate property tag' + _order = 'name' + _sql_constraints = [ + ('name_unique', 'UNIQUE (name)', 'make sure tag name is unique.') + ] + name = fields.Char('Name', required=True) + color = fields.Integer('Colour') diff --git a/realestatinator/models/estate_property_type.py b/realestatinator/models/estate_property_type.py new file mode 100644 index 00000000000..1327e76c8c2 --- /dev/null +++ b/realestatinator/models/estate_property_type.py @@ -0,0 +1,22 @@ +from odoo import api, fields, models + + +class EstatePropertyType(models.Model): + _name = 'estate.property.type' + _description = 'real estate property type' + _order = 'name, sequence' + _sql_constraints = [ + ('name_unique', 'UNIQUE (name)', 'make sure type name is unique.') + ] + + name = fields.Char('Name', required=True) + property_ids = fields.One2many('estate.property', 'property_type_id', string='Property Type') + sequence = fields.Integer('sequence', default=1) + offer_ids = fields.One2many('estate.property.offer', 'property_type_id', string='Offers') + offer_count = fields.Integer(string='Offer Count', compute='_count_offers') + + + @api.depends('offer_ids') + def _count_offers(self): + for record in self: + record.offer_count = len(record.offer_ids) diff --git a/realestatinator/models/res_partner.py b/realestatinator/models/res_partner.py new file mode 100644 index 00000000000..ba42cb0213e --- /dev/null +++ b/realestatinator/models/res_partner.py @@ -0,0 +1,8 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models +from odoo.osv import expression + + +class Partner(models.Model): + _inherit = 'res.partner' diff --git a/realestatinator/models/res_users.py b/realestatinator/models/res_users.py new file mode 100644 index 00000000000..ec4b54f380d --- /dev/null +++ b/realestatinator/models/res_users.py @@ -0,0 +1,9 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class Users(models.Model): + _inherit = 'res.users' + + property_ids = fields.One2many('estate.property', 'sales_person', string='Properties') diff --git a/realestatinator/security/ir.model.access.csv b/realestatinator/security/ir.model.access.csv new file mode 100644 index 00000000000..417deb122c9 --- /dev/null +++ b/realestatinator/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 +realestatinator.access_estate_property,access_estate_property,realestatinator.model_estate_property,base.group_user,1,1,1,1 +realestatinator.access_estate_property_type,access_estate_property_type,realestatinator.model_estate_property_type,base.group_user,1,1,1,1 +realestatinator.access_estate_property_tags,access_estate_property_tags,realestatinator.model_estate_property_tags,base.group_user,1,1,1,1 +realestatinator.access_estate_property_offer,access_estate_property_offer,realestatinator.model_estate_property_offer,base.group_user,1,1,1,1 diff --git a/realestatinator/views/estate_menus.xml b/realestatinator/views/estate_menus.xml new file mode 100644 index 00000000000..9733dff5122 --- /dev/null +++ b/realestatinator/views/estate_menus.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/realestatinator/views/estate_property_offer_views.xml b/realestatinator/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..4e5716552ad --- /dev/null +++ b/realestatinator/views/estate_property_offer_views.xml @@ -0,0 +1,27 @@ + + + + estate.property.offer.action + estate.property.offer + list,form + [('property_type_id', '=', active_id)] + + + estate.property.offer.view.list + estate.property.offer + + + + +