diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000000..13566b81b01 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 00000000000..e60a7fab67c --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,12 @@ + + + + + postgresql + true + org.postgresql.Driver + jdbc:postgresql://localhost:5432/odoo + $ProjectFileDir$ + + + \ No newline at end of file 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/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000000..7f95f45e8e7 --- /dev/null +++ b/.idea/misc.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..55eac5d0bdb --- /dev/null +++ b/.idea/tutorials.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000000..35eb1ddfbbc --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py index 31406e8addb..75024d3a36b 100644 --- a/awesome_dashboard/__manifest__.py +++ b/awesome_dashboard/__manifest__.py @@ -24,7 +24,11 @@ 'assets': { 'web.assets_backend': [ 'awesome_dashboard/static/src/**/*', + ('remove', 'awesome_dashboard/static/src/dashboard/**/*') ], + '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 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 b/awesome_dashboard/static/src/dashboard/dashboard.js new file mode 100644 index 00000000000..ac47f1e478d --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -0,0 +1,94 @@ +/** @odoo-module **/ + +import { Component, useState } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { Layout } from '@web/search/layout'; +import { useService } from "@web/core/utils/hooks"; +import { DashboardItem } from "./dashboard_item"; +import { items } from "./dashboard_items"; +import { NumberCard } from "./number_card"; +import { PieChartCard } from "./pie_chart_card"; +import { Dialog } from "@web/core/dialog/dialog"; +import { CheckBox } from "@web/core/checkbox/checkbox"; +import { browser } from "@web/core/browser/browser"; + + + +class AwesomeDashboard extends Component { + static components = {DashboardItem, NumberCard, PieChartCard, Layout} + static template = "awesome_dashboard.AwesomeDashboard"; + + setup() { + this.state = useState({ + disabledItems: browser.localStorage.getItem("disabledDashboardItems")?.split(",") || [] + }); + this.action = useService("action"); + this.statisticsService = useService("awesome_dashboard.statistics"); + this.dialog = useService("dialog"); + this.statistics = useState(this.statisticsService.statistics); + this.items = registry.category("awesome_dashboard").getAll(); + } + + openConfiguration() { + this.dialog.add(ConfigurationDialog, { + items: this.items, + disabledItems: this.state.disabledItems, + onUpdateConfiguration: this.updateConfiguration.bind(this), + }) + } + + updateConfiguration(disabledItems) { + this.state.disabledItems = disabledItems; + } + + openCustomers() { + this.action.doAction("base.action_partner_form"); + } + + openLeads() { + this.action.doAction({ + type: "ir.actions.act_window", + name: "Leads", + res_model: "crm.lead", + views: [[false, "list"], [false, "form"]], + target: "current", + }); + } +} + + +class ConfigurationDialog extends Component { + static components = { Dialog, CheckBox }; + static template = "awesome_dashboard.ConfigurationDialog"; + static props = ["close", "items", "disabledItems", "onUpdateConfiguration"]; + + setup() { + this.items = useState(this.props.items.map((item) => { + return { + ...item, + enabled: !this.props.disabledItems.includes(item.id), + } + })); + } + + done() { + this.props.close(); + } + + onChange(checked, changedItem) { + changedItem.enabled = checked; + const newDisabledItems = Object.values(this.items).filter( + (item) => !item.enabled + ).map((item) => item.id) + + browser.localStorage.setItem( + "disabledDashboardItems", + newDisabledItems, + ); + + this.props.onUpdateConfiguration(newDisabledItems); + } +} + + +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..1ee54bd1063 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.scss @@ -0,0 +1,3 @@ +.o_dashboard { + background-color: gray; +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml new file mode 100644 index 00000000000..06d8cd239ca --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -0,0 +1,42 @@ + + + + +
+ + + + + +
+ +
+ + + + + + +
+
+
+ + + + Which cards do you whish to see ? + + + + + + + + + + + +
diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item.js b/awesome_dashboard/static/src/dashboard/dashboard_item.js new file mode 100644 index 00000000000..a4f665bfb72 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item.js @@ -0,0 +1,8 @@ +/** @odoo-module **/ + +import { Component } from "@odoo/owl"; + +export class DashboardItem extends Component { + static props = {size: {type: Number, default: 1}, slots: {type: Object}} + static template = "awesome_dashboard.dashboard_item"; +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item.xml b/awesome_dashboard/static/src/dashboard/dashboard_item.xml new file mode 100644 index 00000000000..63fc4d676c0 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item.xml @@ -0,0 +1,12 @@ + + + + +
+
+ +
+
+
+ +
\ 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..4e29a2b75c6 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js @@ -0,0 +1,41 @@ +/** @odoo-module **/ + +import { NumberCard } from "./number_card"; +import { PieChartCard } from "./pie_chart_card"; +import { registry } from "@web/core/registry"; + +const items = [ + { + id: "cancelled_orders", + description: "Cancelled orders this month", + Component: NumberCard, + props: (data) => ({ + title: "Number of cancelled orders this month", + value: data.nb_cancelled_orders, + }) + }, + { + id: "amount_new_orders", + description: "amount orders this month", + Component: NumberCard, + props: (data) => ({ + title: "Total amount of new orders this month", + value: data.total_amount, + }) + }, + { + id: "pie_chart", + description: "Shirt orders by size", + Component: PieChartCard, + size: 2, + props: (data) => ({ + title: "Shirt orders by size", + values: data.orders_by_size, + }) + }, +]; + + +items.forEach(item => { + registry.category("awesome_dashboard").add(item.id, item); +}); \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/number_card.js b/awesome_dashboard/static/src/dashboard/number_card.js new file mode 100644 index 00000000000..1e4c502176b --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/number_card.js @@ -0,0 +1,12 @@ +/** @odoo-module **/ + +import { Component } from "@odoo/owl"; + + +export class NumberCard extends Component { + static template = "awesome_dashboard.number_card"; + static props = { + title: {type: String}, + value: {type: Number}, + }; +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/number_card.xml b/awesome_dashboard/static/src/dashboard/number_card.xml new file mode 100644 index 00000000000..3643b975bb2 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/number_card.xml @@ -0,0 +1,9 @@ + + + + + +

+
+ +
\ 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..bd4449fc91f --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart.js @@ -0,0 +1,58 @@ +/** @odoo-module **/ + +import { Component, onWillStart, onMounted, onWillUpdateProps } from "@odoo/owl"; +import { loadJS } from "@web/core/assets"; + + +export class PieChart extends Component { + static props = {sizes: {type: Object}} + static template = "awesome_dashboard.pie_chart"; + + setup() { + onWillStart(async () => { + await loadJS("/web/static/lib/Chart/Chart.js"); + }); + + onWillUpdateProps((props) => { + this.chart.destroy(); + this.renderChart(props); + }); + + onMounted(() => { + this.renderChart(this.props); + }); + } + + renderChart(props) { + const ctx = document.getElementById('myChart'); + const sizeData = props.sizes; // Assumes response like { sizes: { S: 100, M: 80, ... } } + const labels = Object.keys(sizeData); + const data = Object.values(sizeData); + + this.chart = new Chart(ctx, { + type: "pie", + data: { + labels: labels, + datasets: [{ + data: data, + backgroundColor: [ + "#FF6384", "#36A2EB", "#FFCE56", "#4BC0C0", "#9966FF" + ], + }], + }, + options: { + responsive: true, + plugins: { + legend: { + position: "bottom", + }, + title: { + display: true, + text: "T-Shirt Sizes Sold", + }, + }, + }, + }); + } + +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/pie_chart.xml b/awesome_dashboard/static/src/dashboard/pie_chart.xml new file mode 100644 index 00000000000..4cf166e46d9 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/pie_chart_card.js b/awesome_dashboard/static/src/dashboard/pie_chart_card.js new file mode 100644 index 00000000000..890f8b12991 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart_card.js @@ -0,0 +1,13 @@ +/** @odoo-module **/ + +import { Component } from "@odoo/owl"; +import {PieChart} from "./pie_chart"; + +export class PieChartCard extends Component { + static components = {PieChart} + static template = "awesome_dashboard.pie_chart_card"; + static props = { + title: {type: String}, + values: {type: Object}, + }; +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/pie_chart_card.xml b/awesome_dashboard/static/src/dashboard/pie_chart_card.xml new file mode 100644 index 00000000000..bd45a128d74 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart_card.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ 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..c6131155de2 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/statistics_service.js @@ -0,0 +1,29 @@ +import { registry } from "@web/core/registry"; +import { memoize } from "@web/core/utils/functions"; +import { rpc } from "@web/core/network/rpc"; +import { reactive } from "@odoo/owl"; + + +export const statisticsService = { + start() { + const statistics = reactive({}); + + async function loadStatistics() { + const data = await rpc("/awesome_dashboard/statistics"); + Object.assign(statistics, data); + } + + loadStatistics(); + + setInterval(() => { + loadStatistics(); + }, 10000); + + return { + statistics, + }; + + }, +}; + +registry.category("services").add("awesome_dashboard.statistics", statisticsService); \ 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..584d04cc2e4 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_action.js @@ -0,0 +1,14 @@ +/** @odoo-module **/ + +import { Component, xml } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { LazyComponent } from "@web/core/assets"; + +export class LazyDashboard extends Component { + static components = { LazyComponent }; + static template = xml` + + `; +} + +registry.category("actions").add("awesome_dashboard.dashboard", LazyDashboard); diff --git a/awesome_owl/__manifest__.py b/awesome_owl/__manifest__.py index 77abad510ef..8aab965487c 100644 --- a/awesome_owl/__manifest__.py +++ b/awesome_owl/__manifest__.py @@ -37,6 +37,12 @@ 'web/static/src/libs/fontawesome/css/font-awesome.css', 'awesome_owl/static/src/**/*', ], + 'awesome_owl.assets_counter': [ + 'awesome_owl/static/src/**/*', + ], + 'awesome_owl.assets_card': [ + 'awesome_owl/static/src/**/*', + ], }, 'license': 'AGPL-3' } diff --git a/awesome_owl/static/src/card.js b/awesome_owl/static/src/card.js new file mode 100644 index 00000000000..59138cbcd2d --- /dev/null +++ b/awesome_owl/static/src/card.js @@ -0,0 +1,16 @@ +/** @odoo-module **/ + +import { Component, useState } from "@odoo/owl"; + +export class Card extends Component { + static template = "awesome_owl.card"; + static props = {title: {type: String}, content: {type: String, optional: true}, slots: {type: Object}}; + + setup() { + this.state = useState({cardIsOpen: false}); + } + + toggleCard() { + this.state.cardIsOpen = !this.state.cardIsOpen; + } +} diff --git a/awesome_owl/static/src/card.xml b/awesome_owl/static/src/card.xml new file mode 100644 index 00000000000..49f023e84d8 --- /dev/null +++ b/awesome_owl/static/src/card.xml @@ -0,0 +1,14 @@ + + + +
+
+
+
+ +
+ +
+
+
+
\ 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..9dde113e0d2 --- /dev/null +++ b/awesome_owl/static/src/counter.js @@ -0,0 +1,19 @@ +/** @odoo-module **/ + +import { Component, useState } from "@odoo/owl"; + +export class Counter extends Component { + static props = {callback: {type: Function, optional: true}} + static template = "awesome_owl.counter"; + + setup() { + this.state = useState({ counter: 0 }); + } + + increment() { + this.state.counter++; + if (this.props.callBack) { + this.props.callback("A message to pass ;)") + } + } +} diff --git a/awesome_owl/static/src/counter.xml b/awesome_owl/static/src/counter.xml new file mode 100644 index 00000000000..df5447e19b5 --- /dev/null +++ b/awesome_owl/static/src/counter.xml @@ -0,0 +1,9 @@ + + + + +

Counter:

+ +
+ +
diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 657fb8b07bb..bd3ffd8ef4f 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,7 +1,11 @@ /** @odoo-module **/ -import { Component } from "@odoo/owl"; +import { Component, useState } from "@odoo/owl"; +import {Counter} from "./counter"; +import {Card} from "./card"; +import {Todolist} from "./todo/todo_list"; export class Playground extends Component { + static components = {Counter, Card, Todolist}; static template = "awesome_owl.playground"; } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..7d58948df46 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -2,9 +2,16 @@ -
- hello world -
+ +

This is just a test content

+ +
+
    +
  • Item A
  • +
  • Item B
  • +
+
+
diff --git a/awesome_owl/static/src/todo/todo_item.js b/awesome_owl/static/src/todo/todo_item.js new file mode 100644 index 00000000000..03dd7600d10 --- /dev/null +++ b/awesome_owl/static/src/todo/todo_item.js @@ -0,0 +1,14 @@ +/** @odoo-module **/ + +import { Component, useState } from "@odoo/owl"; + +export class TodoItem extends Component { + static template = "awesome_owl.todo_item"; + static props = { + id: {type: Number}, + description: {type: String}, + isCompleted: {type: Boolean}, + onStatusChange: {type: Function}, + deleteTodoItem: {type: Function} + } +} diff --git a/awesome_owl/static/src/todo/todo_item.xml b/awesome_owl/static/src/todo/todo_item.xml new file mode 100644 index 00000000000..0d770a41ccc --- /dev/null +++ b/awesome_owl/static/src/todo/todo_item.xml @@ -0,0 +1,14 @@ + + + +
+
+ +

+ +

+ +
+
+
+
\ No newline at end of file diff --git a/awesome_owl/static/src/todo/todo_list.js b/awesome_owl/static/src/todo/todo_list.js new file mode 100644 index 00000000000..36a7469ec09 --- /dev/null +++ b/awesome_owl/static/src/todo/todo_list.js @@ -0,0 +1,34 @@ +/** @odoo-module **/ + +import { Component, useState, useRef } from "@odoo/owl"; +import { TodoItem } from './todo_item' + +export class Todolist extends Component { + static components = {TodoItem}; + static template = "awesome_owl.todo_list"; + + setup() { + this.state = useState({idCounter: 1, todoItems: []}) + this.inputRef = useRef('input'); + } + + addTodo(ev) { + if (ev.keyCode === 13 && ev.target.value) { + let newTodoItem = {id: this.state.idCounter++, description: ev.target.value, isCompleted: false} + this.state.todoItems = [...this.state.todoItems, newTodoItem]; + } + } + + inputFocus() { + this.inputRef.el.focus() + } + + onStatusChange(id) { + const todoItemToToggle = this.state.todoItems.find(obj => obj.id === id); + todoItemToToggle.isCompleted = !todoItemToToggle.isCompleted; + } + + deleteTodoItem(id) { + this.state.todoItems = this.state.todoItems.filter(obj => obj.id !== id); + } +} diff --git a/awesome_owl/static/src/todo/todo_list.xml b/awesome_owl/static/src/todo/todo_list.xml new file mode 100644 index 00000000000..1e5ddec3b77 --- /dev/null +++ b/awesome_owl/static/src/todo/todo_list.xml @@ -0,0 +1,15 @@ + + + + +
+ +
+
+ + + +
+
+ +
\ No newline at end of file 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..066ed8169c9 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,14 @@ +{ + 'name': 'ESTATE', + 'depends': ['base'], + 'category': 'Real Estate/Brokerage', + 'data': [ + 'security/ir.model.access.csv', + 'views/estate_property_views.xml', + 'views/estate_menus.xml' + ], + 'installable': True, + 'application': True, + 'auto_install': False, + 'license': 'LGPL-3', +} diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..57665808195 --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1,5 @@ +from . import estate_property +from . import estate_property_type +from . import estate_property_tag +from . import estate_property_offer +from . import estate_property_extend_users diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..929abfc5941 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,106 @@ +from odoo import api, fields, models, _ +from odoo.tools import date_utils, float_utils +from odoo.exceptions import UserError, ValidationError + +class EstateProperty(models.Model): + _name = "estate.property" + _description = "Estate property" + _sql_constraints = [ + ('check_expected_price', 'CHECK(expected_price > 0)', 'The expected price of an property should be positive.'), + ('check_selling_price', 'CHECK(selling_price > 0)', 'The selling price of an property should be positive.') + ] + _order = "id desc" + + + name = fields.Char(string='Estate Name', required=True) + description = fields.Text(string='Description') + postcode = fields.Char(string='Postcode') + date_available = fields.Date(string='Available From Date', copy=False, default=date_utils.add(fields.Date.today(), months=3)) + expected_price = fields.Float(string='Expected Price', required=True) + selling_price = fields.Float(string='Selling Price', readonly=True, copy=False) + bedrooms = fields.Integer(string='Bedrooms', default=2) + living_area = fields.Integer(string='Living Area (sqm)') + facades = fields.Integer(string='Facades') + garage = fields.Boolean(string='Has a Garage') + garden = fields.Boolean(string='Has a Garden') + garden_area = fields.Integer(string='Garden Area (sqm)') + garden_orientation = fields.Selection(string='Garden Orientation', selection=[ + ('north', "North"), + ('south', "South"), + ('east', "East"), + ('west', "West") + ]) + active = fields.Boolean(string='Active', default=True) + state = fields.Selection(string='State', required=True, copy=False, default='new', selection=[ + ('new', "New"), + ('offer_received', "Offer Received"), + ('offer_accepted', "Offer Accepted"), + ('sold', "Sold"), + ('cancelled', "Cancelled") + ]) + + type_id = fields.Many2one("estate.property.type", string="Property Type") + salesperson_id = fields.Many2one("res.users", string="Salesman", default=lambda self: self.env.user) + buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False) + tag_ids = fields.Many2many("estate.property.tag", string="Tags") + offer_ids = fields.One2many("estate.property.offer", 'property_id', string="Offers") + + total_area = fields.Integer(compute="_compute_total_area", string="Total Area (sqm)") + best_price = fields.Float(compute="_compute_best_price", string="Best Price") + + @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: + record.best_price = max(record.offer_ids.mapped('price'), default=0) + + @api.onchange("garden") + def _onchange_garden(self): + if self.garden: + self.garden_orientation = "north" + self.garden_area = 10 + else: + self.garden_orientation = None + self.garden_area = 0 + + @api.onchange("offer_ids") + def _onchange_offers(self): + if len(self.offer_ids) != 0: + self.state = 'offer_received' + else: + self.state = 'new' + + @api.constrains("expected_price", "selling_price") + def check_selling_price(self): + # This function check that the selling price is not lower than 90% of the expected price + for record in self: + if (not float_utils.float_is_zero(record.selling_price, precision_digits=2) + and float_utils.float_compare(record.selling_price, record.expected_price * 0.9, precision_digits=2) < 0): + raise ValidationError("Selling price can't be lower than 90% of expected price") + + @api.ondelete(at_uninstall=False) + def _unlink_except_active_properties(self): + if any(record.state not in ['new', 'cancelled'] for record in self): + raise UserError("Can't delete ongoing property selling.") + + def action_property_sold(self): + for record in self: + if record.state == "cancelled": + raise UserError("Cancelled properties can't be sold.") + else: + record.state = "sold" + + return True + + def action_property_cancel(self): + for record in self: + if record.state == "sold": + raise UserError("Sold properties can't be cancelled.") + else: + record.state = "cancelled" + + return True diff --git a/estate/models/estate_property_extend_users.py b/estate/models/estate_property_extend_users.py new file mode 100644 index 00000000000..422c875abe3 --- /dev/null +++ b/estate/models/estate_property_extend_users.py @@ -0,0 +1,8 @@ +from odoo import fields, models + +class EstatePropertyExtendUsers(models.Model): + _inherit = "res.users" + + + property_ids = fields.One2many("estate.property", 'salesperson_id', string="User Properties", + domain=[('state', 'in', ['new', 'offer_received'])]) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..1fd2cb5f427 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,69 @@ +from odoo import api, fields, models +from odoo.tools import date_utils +from odoo.exceptions import UserError, ValidationError + +class EstatePropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "Estate property offer" + _sql_constraints = [ + ('check_offer_price', 'CHECK(price > 0)', 'The offer price should be positive.') + ] + _order = "price desc" + + + price = fields.Float(string="Price") + status = fields.Selection(string="Status", copy=False, selection=[ + ('accepted', "Accepted"), + ('refused', "Refused"), + ]) + partner_id = fields.Many2one("res.partner", string="Partner", required=True) + property_id = fields.Many2one("estate.property", string="Property", required=True) + + validity = fields.Integer(string="Validity (days)", default=7) + date_deadline = fields.Date(compute="_compute_deadline_date", inverse="_inverse_deadline_date", string="Deadline") + + property_type_id = fields.Many2one(related="property_id.type_id", string="Property Type") + + @api.depends("validity", "property_id.create_date") + def _compute_deadline_date(self): + for record in self: + if record.property_id.create_date: + record.date_deadline = date_utils.add(record.property_id.create_date, days=record.validity) + else: + record.date_deadline = fields.Date.today() + + + @api.depends("date_deadline", "property_id.create_date") + def _inverse_deadline_date(self): + for record in self: + record.validity = (record.date_deadline - record.property_id.create_date.date()).days + + @api.model + def create(self, vals): + new_offer_price = vals.get('price') + property_id = vals.get('property_id') + if property_id: + existing_offers = self.env['estate.property'].browse(property_id).offer_ids + max_price = max(existing_offers.mapped('price'), default=0) + + if new_offer_price <= max_price: + raise ValidationError("New offer price must be higher than existing offers.") + + return super().create(vals) + + def action_accept_offer(self): + for record in self: + record.status = "accepted" + record.property_id.buyer_id = record.partner_id + record.property_id.selling_price = record.price + record.property_id.state = "offer_accepted" + + (self.property_id.offer_ids - self).write({'status': 'refused'}) # refusing all other offers + + return True + + def action_refuse_offer(self): + for record in self: + record.status = "refused" + + return True diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..420e7faa4a5 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,12 @@ +from odoo import fields, models +from odoo.tools import date_utils + +class EstatePropertyTag(models.Model): + _name = "estate.property.tag" + _description = "Estate property tag" + _sql_constraints = [('unique_name', 'UNIQUE(name)', 'tag name must be unique.')] + _order = "name" + + + name = fields.Char(string='Tag', required=True) + color = fields.Integer(string='Color') diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..f12504aa437 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,19 @@ +from odoo import api, fields, models + +class EstatePropertyType(models.Model): + _name = "estate.property.type" + _description = "Estate property type" + _sql_constraints = [('unique_name', 'UNIQUE(name)', 'property type must be unique.')] + _order = "name" + + + name = fields.Char(string='Property Type', required=True) + property_ids = fields.One2many('estate.property', 'type_id', string="Properties") + sequence = fields.Integer('Sequence', help="Used to order types. higher is better.") + offer_ids = fields.One2many('estate.property.offer', 'property_type_id', string="Offers") + offer_count = fields.Integer(compute="_compute_offer_count", string="Offer Count") + + @api.depends("offer_ids") + def _compute_offer_count(self): + for record in self: + record.offer_count = len(record.offer_ids) \ No newline at end of file diff --git a/estate/security/estate_security.xml b/estate/security/estate_security.xml new file mode 100644 index 00000000000..7578acaf9bc --- /dev/null +++ b/estate/security/estate_security.xml @@ -0,0 +1,25 @@ + + + + + Agent + + + + + Manager + + + + + + A description of the rule's role + + + + [ '|', ('create_uid', '=', user.id), + ('create_uid', '=', False) ] + + + + diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..ef6de984031 --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,5 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_estate_property_user,estate.property.access.user,model_estate_property,base.group_user,1,1,1,1 +access_estate_property_type_user,estate.property.type.access.user,model_estate_property_type,base.group_user,1,1,1,1 +access_estate_property_tag_user,estate.property.tag.access.user,model_estate_property_tag,base.group_user,1,1,1,1 +access_estate_property_offer_user,estate.property.offer.access.user,model_estate_property_offer,base.group_user,1,1,1,1 diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..ac58581df6e --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..1f0c62c0985 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,259 @@ + + + + + + Properties + estate.property + list,form,kanban + {'search_default_state': True} + + + + Property Types + estate.property.type + list,form + + + + Property Tags + estate.property.tag + list,form + + + + Estate Property Offers + estate.property.offer + list,form + [('property_type_id', '=', active_id)] + + + + estate.property.search + estate.property + + + + + + + + + + + + + + + + + + + estate.property.list + estate.property + + + + + + + + + + + + + + + + + estate.property.kanban + estate.property + + + + + +
+
+

+ +

+ +
+ Expected Price: + +
+ +
+ Best Offer: + +
+ +
+ Selling Price: + +
+
+
+
+
+
+
+
+ + + estate.property.form + estate.property + +
+
+
+ +
+

+ +

+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + estate.property.type.list + estate.property.type + + + + + + + + + + estate.property.type.form + estate.property.type + +
+ +
+ +
+
+

+ +

+
+ + + + + + + + + + + +
+
+
+
+ + + res.users.view.form.inherit.estate.property + res.users + + + + + + + + + + + + + + + + + + + + + + + + estate.property.offer.list + estate.property.offer + + + + + +