diff --git a/.gitignore b/.gitignore index b6e47617de1..65a3e9c663f 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,10 @@ dmypy.json # Pyre type checker .pyre/ + +.eslintignore +.eslintrc.json +jsconfig.json +package.json +package-lock.json +node_modules/ diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py index a1cd72893d7..995e6ac1119 100644 --- a/awesome_dashboard/__manifest__.py +++ b/awesome_dashboard/__manifest__.py @@ -25,6 +25,9 @@ 'web.assets_backend': [ 'awesome_dashboard/static/src/**/*', ], + 'awesome_dashboard.dashboard': [ + 'awesome_dashboard/static/src/dashboard/**/*', + ], }, 'license': 'AGPL-3' } diff --git a/awesome_dashboard/static/dashboard.png b/awesome_dashboard/static/dashboard.png new file mode 100644 index 00000000000..be75c2ef5f2 Binary files /dev/null and b/awesome_dashboard/static/dashboard.png differ 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/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js new file mode 100644 index 00000000000..8e49cc3e184 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -0,0 +1,91 @@ +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 "./dashboarditem/dashboarditem"; +import { PieChart } from "./piechart/piechart"; + +class AwesomeDashboard extends Component { + static template = "awesome_dashboard.AwesomeDashboard"; + static components = { Layout, DashboardItem, PieChart } + + setup() { + this.action = useService("action"); + const statisticsService = useService("awesome_dashboard.statistics"); + this.statistics = useState(statisticsService.state); + this.modalState = useState({ isOpen: false }); + this.hiddenItems = useState({ + ids: JSON.parse( + localStorage.getItem("hidden_dashboard_items") || "[]" + ), + }); + this.items = useState( + registry + .category("awesome_dashboard") + .getAll() + .filter((item) => !this.hiddenItems.ids.includes(item.id)) + ); + this.allItems = useState( + registry + .category("awesome_dashboard") + .getAll() + ); + } + + openCustomers() { + this.action.doAction({ + type: "ir.actions.act_window", + name: "Customers", + res_model: "res.partner", + views: [ + [false, "kanban"], + [false, "form"], + [false, "list"], + ], + }); + } + + openLeads() { + this.action.doAction({ + type: "ir.actions.act_window", + name: "Leads", + res_model: "crm.lead", + views: [ + [false, "list"], + [false, "form"], + ], + }); + } + + toggleModal() { + this.modalState.isOpen = !this.modalState.isOpen + } + + toggleItem(event) { + const itemId = event.target.value; + + if (event.target.checked) { + this.hiddenItems.ids = this.hiddenItems.ids.filter( + (id) => id !== itemId + ); + } else { + this.hiddenItems.ids.push(itemId); + } + } + + applySettings() { + localStorage.setItem( + "hidden_dashboard_items", + JSON.stringify(this.hiddenItems.ids) + ); + + this.items = registry + .category("awesome_dashboard") + .getAll() + .filter((item) => !this.hiddenItems.ids.includes(item.id)); + + this.toggleModal(); + } +} + +registry.category("lazy_components").add("awesome_dashboard.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..347a4416603 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.scss @@ -0,0 +1,3 @@ +.o_dashboard { + background-color:rgb(27, 29, 38) +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml new file mode 100644 index 00000000000..880a0c16027 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + +
+ + + + + + +
+ + +
+
+ +
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..d2667a75834 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js @@ -0,0 +1,67 @@ +import { registry } from "@web/core/registry"; +import { NumberCard } from "./numbercard/numbercard"; +import { PieChartCard } from "./piechartcard/piechartcard"; + +export const items = [ + { + id: "average_quantity", + description: "Average amount of t-shirt", + Component: NumberCard, + props: (data) => ({ + title: "Average amount of t-shirt by order this month", + value: data.average_quantity, + }), + }, + { + id: "nb_new_orders", + description: "Number of new orders this month", + Component: NumberCard, + props: (data) => ({ + title: "Number of new orders this month", + value: data.nb_new_orders, + }), + }, + { + id: "nb_cancelled_orders", + description: "Number of cancelled orders this month", + Component: NumberCard, + props: (data) => ({ + title: "Number of cancelled orders this month", + value: data.nb_cancelled_orders, + }), + }, + { + id: "total_amount", + description: "Total amount of new orders this month", + Component: NumberCard, + 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, + props: (data) => ({ + title: "Shirt orders by size", + value: data.orders_by_size, + }), + }, + { + id: "average_time", + description: + "Average time for an order to go from ‘new’ to ‘sent’ or ‘cancelled’", + Component: NumberCard, + props: (data) => ({ + title: "Average time for an order to go from ‘new’ to ‘sent’ or ‘cancelled’", + value: data.average_time, + }), + }, +]; + +items.forEach((item) => { + registry + .category("awesome_dashboard") + .add("awesome_dashboard.dashboard_item." + item.id, item); +}); diff --git a/awesome_dashboard/static/src/dashboard/dashboarditem/dashboarditem.js b/awesome_dashboard/static/src/dashboard/dashboarditem/dashboarditem.js new file mode 100644 index 00000000000..727bd63e1a8 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboarditem/dashboarditem.js @@ -0,0 +1,19 @@ +import { Component } from "@odoo/owl"; + +export class DashboardItem extends Component { + static template = "awesome_dashboard.DashboardItem"; + + static props = { + slots: { + type: Object, + shape: { + default: Object, + }, + }, + size: { + type: Number, + default: 1, + optional: true, + }, + }; +} diff --git a/awesome_dashboard/static/src/dashboard/dashboarditem/dashboarditem.xml b/awesome_dashboard/static/src/dashboard/dashboarditem/dashboarditem.xml new file mode 100644 index 00000000000..f7c20cb5c08 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboarditem/dashboarditem.xml @@ -0,0 +1,12 @@ + + + + +
+
+ +
+
+
+ +
diff --git a/awesome_dashboard/static/src/dashboard/numbercard/numbercard.js b/awesome_dashboard/static/src/dashboard/numbercard/numbercard.js new file mode 100644 index 00000000000..3028b0c429e --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/numbercard/numbercard.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/numbercard/numbercard.xml b/awesome_dashboard/static/src/dashboard/numbercard/numbercard.xml new file mode 100644 index 00000000000..614c9267c4c --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/numbercard/numbercard.xml @@ -0,0 +1,7 @@ + + + +
+

+ + 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..3a40eaa9fb5 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/piechart/piechart.js @@ -0,0 +1,76 @@ +import { Component, onWillStart, onPatched, useRef, onMounted } from "@odoo/owl"; +import { loadJS } from "@web/core/assets"; + +export class PieChart extends Component { + static template = "awesome_dashboard.PieChart"; + static props = { + items: { + type: Object, + optional: true, + default: () => { }, + }, + } + + setup() { + this.chartRef = useRef("pie-canvas"); + this.myPieChart = null; + + onWillStart(async () => { + await loadJS("/web/static/lib/Chart/Chart.js"); + }); + + onMounted(() => { + this.renderChart() + }); + + onPatched(() => { + this.renderChart() + }); + } + + renderChart() { + if (!this.chartRef.el) { + return; + } + + if (this.myPieChart) { + this.myPieChart.destroy(); + } + + this.pieChartData = { + labels: ['M', 'S', 'XL'], + datasets: [{ + label: 'Sales Count', + data: [this.props.items.m, this.props.items.s, this.props.items.xl], + backgroundColor: [ + 'rgba(255, 99, 132, 0.6)', + 'rgba(54, 162, 235, 0.6)', + 'rgba(255, 206, 86, 0.6)' + ], + borderColor: [ + 'rgba(255, 99, 132, 1)', + 'rgba(54, 162, 235, 1)', + 'rgba(255, 206, 86, 1)' + ], + borderWidth: 1 + }] + }; + + this.pieChartOptions = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'top', + } + } + }; + + this.myPieChart = new Chart(this.chartRef.el, + { + type: 'pie', + data: this.pieChartData, + options: this.pieChartOptions + }); + } +} 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..811eb8464be --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/piechart/piechart.xml @@ -0,0 +1,10 @@ + + + + +

+ +
+ + + diff --git a/awesome_dashboard/static/src/dashboard/piechartcard/piechartcard.js b/awesome_dashboard/static/src/dashboard/piechartcard/piechartcard.js new file mode 100644 index 00000000000..a1ba011eb2f --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/piechartcard/piechartcard.js @@ -0,0 +1,12 @@ +import { Component } from "@odoo/owl"; +import { PieChart } from "../piechart/piechart"; + +export class PieChartCard extends Component { + static template = "awesome_dashboard.PieChartCard"; + static components = { PieChart }; + + static props = { + title: String, + value: Object, + }; +} diff --git a/awesome_dashboard/static/src/dashboard/piechartcard/piechartcard.xml b/awesome_dashboard/static/src/dashboard/piechartcard/piechartcard.xml new file mode 100644 index 00000000000..4089bfe8963 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/piechartcard/piechartcard.xml @@ -0,0 +1,7 @@ + + + +
+ + + diff --git a/awesome_dashboard/static/src/dashboard/statistics_services.js b/awesome_dashboard/static/src/dashboard/statistics_services.js new file mode 100644 index 00000000000..273df0591dd --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/statistics_services.js @@ -0,0 +1,21 @@ +import { registry } from "@web/core/registry"; +import { rpc } from "@web/core/network/rpc"; +import { reactive } from "@odoo/owl"; + +const statisticsService = { + async: ["loadStatistics"], + start() { + const loadStatistics = () => rpc("/awesome_dashboard/statistics") + const state = reactive({ data: null }); + const fetchData = async () => { + state.data = await loadStatistics(); + }; + fetchData(); + setInterval(fetchData, 10 * 60 * 1000); + return { + state: state + }; + }, +}; + +registry.category("services").add("awesome_dashboard.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..4d160755898 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_action.js @@ -0,0 +1,16 @@ +import { Component, xml } from "@odoo/owl"; +import { LazyComponent } from "@web/core/assets"; +import { registry } from "@web/core/registry"; + +export class DashboardAction extends Component { + static components = { LazyComponent }; + + static template = xml` + + `; +} + +registry.category("actions").add("awesome_dashboard.dashboard", DashboardAction); diff --git a/awesome_dashboard/views/views.xml b/awesome_dashboard/views/views.xml index 47fb2b6f258..87951a4bcc8 100644 --- a/awesome_dashboard/views/views.xml +++ b/awesome_dashboard/views/views.xml @@ -5,7 +5,7 @@ awesome_dashboard.dashboard - + diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js new file mode 100644 index 00000000000..ca02b8e5689 --- /dev/null +++ b/awesome_owl/static/src/card/card.js @@ -0,0 +1,25 @@ +import { Component, useState } from "@odoo/owl"; + + +export class Card extends Component { + static template = "awesome_owl.card"; + static props = { + title: String, + slots: { + type: Object, + shape: { + default: {} + } + } + }; + + setup() { + this.state = useState([{ + isOpen: false + }]) + } + + toggleCard() { + this.state.isOpen = !this.state.isOpen + } +} diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml new file mode 100644 index 00000000000..cf89a968d14 --- /dev/null +++ b/awesome_owl/static/src/card/card.xml @@ -0,0 +1,32 @@ + + + +
+ +
+

+ +

+
+ +
+
+
+
+
diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js new file mode 100644 index 00000000000..6e3681e0f89 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.js @@ -0,0 +1,31 @@ +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: 0 }); + } + + increment() { + this.state.value++; + if (this.props.onChange) { + this.props.onChange(1); + } + } + + decrement() { + this.state.value--; + if (this.props.onChange) { + this.props.onChange(-1); + } + } + + reset() { + this.state.value = 0; + } +} diff --git a/awesome_owl/static/src/counter/counter.xml b/awesome_owl/static/src/counter/counter.xml new file mode 100644 index 00000000000..21173bb083d --- /dev/null +++ b/awesome_owl/static/src/counter/counter.xml @@ -0,0 +1,26 @@ + + + + +
+

Counter:

+ + + +
+
+ +
diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 4ac769b0aa5..f1294597071 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,5 +1,30 @@ -import { Component } from "@odoo/owl"; +import { markup, Component, useState } from "@odoo/owl"; +import { Counter } from "./counter/counter"; +import { Card } from "./card/card"; +import { TodoList } from "./todo/todolist"; + export class Playground extends Component { static template = "awesome_owl.playground"; + static components = { Counter, Card, TodoList }; + + setup() { + this.state = useState({ + content: markup('

Welcome to your Counter!

'), + sum: 0 + }); + } + + calculateSum(newValue) { + if (newValue > 0) { + this.state.sum++; + } + if (newValue < 0) { + this.state.sum--; + } + } + + reset() { + this.state.sum = 0 + } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..61f0e28d667 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -2,8 +2,41 @@ -
- hello world + +
+ + + +
+
+

Sum:

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

+ Hello World +

+
+
diff --git a/awesome_owl/static/src/todo/todoitem.js b/awesome_owl/static/src/todo/todoitem.js new file mode 100644 index 00000000000..70b10e8ba21 --- /dev/null +++ b/awesome_owl/static/src/todo/todoitem.js @@ -0,0 +1,19 @@ +import { Component } from "@odoo/owl"; + + +export class TodoItem extends Component { + static template = "awesome_owl.todoitem"; + static props = { + todo: Object, + toggleState: Function, + removeTodo: Function + }; + + toggleIsCompleted() { + this.props.toggleState(this.props.todo.id) + } + + onClickRemove() { + this.props.removeTodo(this.props.todo.id) + } +} diff --git a/awesome_owl/static/src/todo/todoitem.xml b/awesome_owl/static/src/todo/todoitem.xml new file mode 100644 index 00000000000..9bbf44a72f8 --- /dev/null +++ b/awesome_owl/static/src/todo/todoitem.xml @@ -0,0 +1,11 @@ + + + +
+ + . + + +
+
+
diff --git a/awesome_owl/static/src/todo/todolist.js b/awesome_owl/static/src/todo/todolist.js new file mode 100644 index 00000000000..141a17d673d --- /dev/null +++ b/awesome_owl/static/src/todo/todolist.js @@ -0,0 +1,39 @@ +import { Component, useState } from "@odoo/owl"; +import { TodoItem } from "./todoitem"; +import { useAutofocus } from "../utils"; + + +export class TodoList extends Component { + static template = "awesome_owl.todolist"; + static components = { TodoItem }; + + setup() { + this.todos = useState([]) + useAutofocus("todo_input") + } + + addTodo(ev) { + if (ev.keyCode == '13') { + if (ev.target.value != "") { + this.todos.push({ + id: this.todos.length + 1, + description: ev.target.value, + isCompleted: false + }) + ev.target.value = '' + } + } + } + + toggleState(id) { + const todo = this.todos.find(todo => todo.id == id) + todo.isCompleted = !todo.isCompleted; + } + + removeTodo(id) { + const index = this.todos.findIndex((elem) => elem.id === id); + if (index >= 0) { + this.todos.splice(index, 1) + } + } +} diff --git a/awesome_owl/static/src/todo/todolist.xml b/awesome_owl/static/src/todo/todolist.xml new file mode 100644 index 00000000000..35cc0d97f0e --- /dev/null +++ b/awesome_owl/static/src/todo/todolist.xml @@ -0,0 +1,44 @@ + + + +
+
+

+ Todo List +

+ +
+
+ +
+
+
+
+
+
diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js new file mode 100644 index 00000000000..fde94eb31da --- /dev/null +++ b/awesome_owl/static/src/utils.js @@ -0,0 +1,13 @@ +import { useRef, onMounted } from "@odoo/owl"; + + +export function useAutofocus(refName) { + const ref = useRef(refName); + + onMounted(() => { + if (ref.el) { + ref.el.focus(); + } + }); + return ref; +} diff --git a/estate/__init__.py b/estate/__init__.py new file mode 100755 index 00000000000..cc73260f076 --- /dev/null +++ b/estate/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 +from . import models diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100755 index 00000000000..7ac532ba3c1 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +{ + "name": "Real Estate", + "description": """ + Real Estate Module to Buy and Sell Your Real Estate with Ease. + """, + "version": "1.0", + "depends": ["base", "sale"], + "author": "danal", + "category": "Category", + "application": True, + "license": "LGPL-3", + "data": [ + "security/ir.model.access.csv", + "views/estate_property_views.xml", + "views/estate_property_offer_views.xml", + "views/estate_property_tag_views.xml", + "views/estate_property_type_views.xml", + "views/res_users_views.xml", + "views/sale_order_views.xml", + "views/estate_menus.xml", + ], +} diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100755 index 00000000000..42e0ea3e7f3 --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python3 +from . import estate_property +from . import estate_property_type +from . import estate_property_tag +from . import estate_property_offer +from . import res_users +from . import sale_order diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100755 index 00000000000..b56f6bc0af3 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 +from dateutil.relativedelta import relativedelta +from odoo.exceptions import UserError, ValidationError + +from odoo import api, models, fields + + +class EstateProperty(models.Model): + _name = "estate.property" + _description = "Estate Property" + _order = "id desc" + + name = fields.Char(required=True) + description = fields.Text() + postcode = fields.Char() + date_avaliability = fields.Date( + copy=False, default=fields.Date.today() + relativedelta(month=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"), + ], + string="Orientation", + ) + active = fields.Boolean(default=True) + state = fields.Selection( + selection=[ + ("new", "New"), + ("offer_received", "Offer Received"), + ("offer_accepted", "Offer Accepted"), + ("sold", "Sold"), + ("cancelled", "Cancelled"), + ], + string="Status", + required=True, + copy=False, + default="new", + ) + property_type_id = fields.Many2one("estate.property.type", string="Property Type", required=True) + customer = fields.Many2one("res.partner", string="Customer", copy=False) + salesperson = fields.Many2one( + "res.users", string="Salesperson", default=lambda self: self.env.user + ) + tag_ids = fields.Many2many("estate.property.tag", string="Property Tags") + offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offer") + total_area = fields.Integer(compute="_compute_total_area") + best_price = fields.Integer(compute="_compute_best_price") + + _check_expected_price = models.Constraint( + "CHECK(expected_price > 0)", + "A property expected price must be strictly positive.", + ) + _check_selling_price = models.Constraint( + "CHECK(selling_price >= 0)", "A property selling price must be positive." + ) + + @api.depends("living_area", "garden_area") + def _compute_total_area(self): + for record in self: + record.total_area = record.living_area + record.garden_area + + @api.depends("offer_ids.price") + def _compute_best_price(self): + for record in self: + if not record.mapped("offer_ids.price"): + record.best_price = 0 + else: + record.best_price = max(record.mapped("offer_ids.price")) + + @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 = None + + @api.onchange("offer_ids") + def _onchange_offer_ids(self): + if len(self.offer_ids) == 0: + self.state = "new" + + def action_sold_property(self): + for record in self: + if record.state == "cancelled": + raise UserError("Cancelled Property cannot be Sold") + else: + record.state = "sold" + return True + + def action_cancel_offer(self): + for record in self: + if record.state == "sold": + raise UserError("Sold Property cannot be Cancelled") + else: + record.state = "cancelled" + return True + + @api.constrains("selling_price", "expected_price") + def _check_selling_price_persentage(self): + for record in self: + selling_price_persentage = ( + record.selling_price / record.expected_price + ) * 100 + if selling_price_persentage >= 90 or selling_price_persentage == 0: + pass + else: + raise ValidationError( + "the selling price cannot be lower than 90% of the expected price." + ) + + @api.ondelete(at_uninstall=False) + def _unlink_prevent_property_on_state(self): + for record in self: + if record.state not in ("new", "cancelled"): + raise UserError( + "Can Delete property only on 'New' or 'Cancelled' state!" + ) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..4c162053e9b --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,70 @@ +from dateutil.relativedelta import relativedelta +from odoo.exceptions import UserError + +from odoo import api, models, fields + + +class EstatePropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "Estate Property Offer" + _order = "price desc" + + price = fields.Float() + status = fields.Selection( + selection=[("accepted", "Accepted"), ("refused", "Refused")], copy=False + ) + partner_id = fields.Many2one("res.partner", string="Partner", required=True) + property_id = fields.Many2one("estate.property", string="Property", required=True) + validity = fields.Integer(default=7) + date_deadline = fields.Date(compute="_compute_deadline", inverse="_inverse_date") + property_type_id = fields.Many2one( + related="property_id.property_type_id", store=True + ) + + _check_offer_price = models.Constraint( + "CHECK(price > 0)", "An offer price must be strictly positive" + ) + + @api.depends("validity") + def _compute_deadline(self): + for record in self: + default_creation_date = record.create_date or fields.Date.today() + record.date_deadline = ( + relativedelta(days=record.validity) + default_creation_date + ) + + def _inverse_date(self): + for record in self: + default_creation_date = record.create_date or fields.Date.today() + record.validity = ( + record.date_deadline - fields.Date.to_date(default_creation_date) + ).days + + def action_accept(self): + for record in self: + record.status = "accepted" + other_offers = record.property_id.offer_ids - record + record.property_id.selling_price = record.price + record.property_id.customer = record.partner_id + record.property_id.state = "offer_accepted" + other_offers.write({"status": "refused"}) + return True + + def action_refuse(self): + for record in self: + record.status = "refused" + record.property_id.customer = None + return True + + @api.model + def create(self, vals): + if len(vals) > 0: + property = self.env["estate.property"].browse(vals[0]["property_id"]) + for record in vals: + if property.state == "new": + property.state = "offer_received" + if record["price"] < property.best_price: + raise UserError( + "Cannot create an offer with a lower amount than an existing offer." + ) + return super().create(vals) diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..3475da956ac --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,14 @@ +from odoo import fields, models + + +class EstatePropertyTags(models.Model): + _name = "estate.property.tag" + _description = "Estate Property Tag" + _order = "name" + + name = fields.Char(required=True) + color = fields.Integer() + + _check_tag_name = models.Constraint( + "UNIQUE(name)", "A property tag name should be unique." + ) diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100755 index 00000000000..d611ccf31f4 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 +from odoo import models, fields, api + + +class EstatePropertyType(models.Model): + _name = "estate.property.type" + _description = "Estate Property Type" + _order = "sequence,name" + + name = fields.Char(required=True) + property_ids = fields.One2many("estate.property", "property_type_id") + sequence = fields.Integer("Sequence", default=1) + offer_ids = fields.One2many("estate.property.offer", "property_type_id") + offer_count = fields.Integer(compute="_compute_offer_count") + + _check_type_name = models.Constraint( + "UNIQUE(name)", "A property type name should be unique." + ) + + @api.depends("offer_ids") + def _compute_offer_count(self): + for record in self: + record.offer_count = len(record.property_ids.mapped("offer_ids")) diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..e47c5671ac9 --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,11 @@ +from odoo import fields, models + + +class ResUser(models.Model): + _inherit = "res.users" + + property_ids = fields.One2many( + "estate.property", + "salesperson", + domain="[('state', '!=', 'sold')]", + ) diff --git a/estate/models/sale_order.py b/estate/models/sale_order.py new file mode 100644 index 00000000000..b286c7acee0 --- /dev/null +++ b/estate/models/sale_order.py @@ -0,0 +1,7 @@ +from odoo import fields, models + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + property_id = fields.Many2one("estate.property") diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100755 index 00000000000..352afe3899f --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,5 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +estate.access_estate_property,"access_estate_property","model_estate_property",base.group_user,1,1,1,1 +estate.access_estate_property_type,"access_estate_property_type","model_estate_property_type",base.group_user,1,1,1,1 +estate.access_estate_property_tag,"access_estate_property_tag","model_estate_property_tag",base.group_user,1,1,1,1 +estate.access_estate_property_offer,"access_estate_property_offer","model_estate_property_offer",base.group_user,1,1,1,1 diff --git a/estate/static/icon.png b/estate/static/icon.png new file mode 100644 index 00000000000..01789b0d784 Binary files /dev/null and b/estate/static/icon.png differ diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100755 index 00000000000..26ccdbbb271 --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,37 @@ + + + + + + + + + diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..a183e7c92d0 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,24 @@ + + + + Property Offers + estate.property.offer + list,form + [('property_type_id', '=', active_id)] + + + + estate.property.offer.list + estate.property.offer + + + + + + + +
+

+ +

+ + + + + + + + + + + + + + + + + + estate.property.type.list + estate.property.type + + + + + + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100755 index 00000000000..bf186e74268 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,154 @@ + + + + Properties + estate.property + kanban,list,form + {'search_default_avaliable': 1} + +

+ Create your first Property! +

+
+
+ + + estate.property.form + estate.property + +
+
+
+ +

+ +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + estate.property.list + estate.property + + + + + + + + + + + + + + + + estate.property.kanban + estate.property + + + + + +
+
+ + +
+
+ Expected Price: + +
+
+ Best Offer: + +
+
+ Selling Price: + +
+ +
+
+
+
+
+
+ + + estate.property.search + estate.property + + + + + + + + + + + + + + + + + +
diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml new file mode 100644 index 00000000000..cd482a99be2 --- /dev/null +++ b/estate/views/res_users_views.xml @@ -0,0 +1,15 @@ + + + + res.users.view.form.inherit + res.users + + + + + + + + + + diff --git a/estate/views/sale_order_views.xml b/estate/views/sale_order_views.xml new file mode 100644 index 00000000000..e43f0b5caf4 --- /dev/null +++ b/estate/views/sale_order_views.xml @@ -0,0 +1,12 @@ + + + sale.order.view.inherit + sale.order + + + + + + + + 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..26928caee63 --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,12 @@ +{ + "name": "Estate Account", + "description": """ + Estate account module to create invoice of sold property. + """, + "version": "1.0", + "depends": ["estate", "account"], + "author": "danal", + "category": "Category", + "license": "LGPL-3", + "data": [], +} 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..9850cdeb6d3 --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,31 @@ +from odoo import models, Command + + +class EstatePropertyInherit(models.Model): + _inherit = "estate.property" + + def action_sold_property(self): + for record in self: + self.env["account.move"].create( + { + "partner_id": record.customer.id, + "move_type": "out_invoice", + "invoice_line_ids": [ + Command.create( + { + "name": "6% of the selling price", + "quantity": 1, + "price_unit": record.selling_price * 0.06, + } + ), + Command.create( + { + "name": "an additional 100.00 from administrative fees", + "quantity": 1, + "price_unit": 100, + } + ), + ], + } + ) + return super().action_sold_property()