diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py index 31406e8addb..c50b9e0c067 100644 --- a/awesome_dashboard/__manifest__.py +++ b/awesome_dashboard/__manifest__.py @@ -1,30 +1,28 @@ # -*- coding: utf-8 -*- { - 'name': "Awesome Dashboard", - - 'summary': """ + "name": "Awesome Dashboard", + "summary": """ Starting module for "Discover the JS framework, chapter 2: Build a dashboard" """, - - 'description': """ + "description": """ Starting module for "Discover the JS framework, chapter 2: Build a dashboard" """, - - 'author': "Odoo", - 'website': "https://www.odoo.com/", - 'category': 'Tutorials/AwesomeDashboard', - 'version': '0.1', - 'application': True, - 'installable': True, - 'depends': ['base', 'web', 'mail', 'crm'], - - 'data': [ - 'views/views.xml', + "author": "Odoo", + "website": "https://www.odoo.com/", + "category": "Tutorials/AwesomeDashboard", + "version": "0.1", + "application": True, + "installable": True, + "depends": ["base", "web", "mail", "crm"], + "data": [ + "views/views.xml", ], - 'assets': { - 'web.assets_backend': [ - 'awesome_dashboard/static/src/**/*', + "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' + "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..54dec402fda --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -0,0 +1,101 @@ +/** @odoo-module **/ + +import { _t } from "@web/core/l10n/translation"; +import { Component, useState } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { Layout } from "@web/search/layout"; +import { useService } from "@web/core/utils/hooks"; +import { DashboardItem } from "./dashboard_item/dashboard_item"; +import { 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 template = "awesome_dashboard.AwesomeDashboard"; + static components = { Layout, DashboardItem }; + + setup() { + this.display = { + controlPanel: {}, + }; + this.action = useService("action"); + + this.orders_details = useState(useService("awesome_dashboard.statistics")); + + this.items = registry.category("awesome_dashboard").getAll(); + + this.dialog = useService("dialog"); + + this.state = useState({ + disabledItems: + browser.localStorage.getItem("disabledDashboardItems")?.split(",") || + [], + }); + } + + openConfiguration() { + this.dialog.add(ConfigurationDialog, { + items: this.items, + disabledItems: this.state.disabledItems, + onUpdateConfiguration: this.updateConfiguration.bind(this), + }); + } + + updateConfiguration(newDisabledItems) { + this.state.disabledItems = newDisabledItems; + } + + showCustomers() { + this.action.doAction("base.action_partner_form"); + } + + showLeads() { + this.action.doAction({ + type: "ir.actions.act_window", + name: _t("Leads"), + target: "current", + res_model: "crm.lead", + views: [ + [false, "list"], + [false, "form"], + ], + }); + } +} + +class ConfigurationDialog extends Component { + + static template = "awesome_dashboard.ConfigurationDialog"; + static components = { Dialog, CheckBox }; + 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("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..6be5e0f83c9 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.scss @@ -0,0 +1,3 @@ +.o_dashboard { + background-color: gray; +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml new file mode 100644 index 00000000000..c67bbfe6b83 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + +
+
+ +
+ + + + +
+
+
+
+
+
+ + + + Which cards do you whish to see ? + + + + + + + + + + + +
diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js new file mode 100644 index 00000000000..4644048af21 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js @@ -0,0 +1,18 @@ +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/dashboard_item/dashboard_item.xml b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml new file mode 100644 index 00000000000..56b1b832a0f --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml @@ -0,0 +1,10 @@ + + + +
+
+ +
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard/dashboard_items.js b/awesome_dashboard/static/src/dashboard/dashboard_items.js new file mode 100644 index 00000000000..300e8bbd53e --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js @@ -0,0 +1,65 @@ +import { NumberCard } from "./number_card/number_card"; +import { PieChartCard } from "./pie_chart_card/pie_chart_card"; +import { registry } from "@web/core/registry"; + +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: "average_time", + description: "Average time for an order", + Component: NumberCard, + props: (data) => ({ + title: "Average time for an order to go from 'new' to 'sent' or 'cancelled'", + value: data.average_time, + }) + }, + { + id: "number_new_orders", + description: "New orders this month", + Component: NumberCard, + props: (data) => ({ + title: "Number of new orders this month", + value: data.nb_new_orders, + }) + }, + { + 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); +}); 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..d3bd9c0e4ef --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/number_card/number_card.js @@ -0,0 +1,13 @@ +import { Component } from "@odoo/owl"; + +export class NumberCard extends Component { + static template = "awesome_dashboard.NumberCard"; + static props = { + title: { + type: String, + }, + value: { + type: Number, + } + } +} \ No newline at end of file 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..3a0713623fa --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/number_card/number_card.xml @@ -0,0 +1,9 @@ + + + + +
+ +
+
+
diff --git a/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js new file mode 100644 index 00000000000..63b0870e620 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js @@ -0,0 +1,37 @@ +import { Component, onWillStart, useState, useRef, onMounted, onWillUnmount } from "@odoo/owl"; +import { loadJS } from "@web/core/assets"; + +export class PieChart extends Component { + static template = "awesome_dashboard.PieChart"; + static props = { + data: Object, + }; + + setup() { + this.canvasRef = useRef("canvas"); + onWillStart(() => loadJS("/web/static/lib/Chart/Chart.js")); + onMounted(() => { + this.renderChart(); + }); + onWillUnmount(() => { + this.chart.destroy(); + }); + } + + renderChart() { + const labels = Object.keys(this.props.data); + const data = Object.values(this.props.data); + this.chart = new Chart(this.canvasRef.el, { + type: "pie", + data: { + labels: labels, + datasets: [ + { + label: this.props.label, + data: data, + }, + ], + }, + }); + } +} diff --git a/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml new file mode 100644 index 00000000000..4f3c54a6c15 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml @@ -0,0 +1,10 @@ + + + +
+
+ +
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.js b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.js new file mode 100644 index 00000000000..3faac175fed --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.js @@ -0,0 +1,15 @@ +import { Component } from "@odoo/owl"; +import { PieChart } from "../pie_chart/pie_chart"; + +export class PieChartCard extends Component { + static template = "awesome_dashboard.PieChartCard"; + static components = { PieChart } + static props = { + title: { + type: String, + }, + values: { + type: Object, + }, + } +} diff --git a/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.xml b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.xml new file mode 100644 index 00000000000..58a6811c83a --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart_card/pie_chart_card.xml @@ -0,0 +1,7 @@ + + + + + + + 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..95c9b53d3e2 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/statistics_service.js @@ -0,0 +1,36 @@ +/** @odoo-module **/ +import { rpc } from "@web/core/network/rpc"; +import { registry } from "@web/core/registry"; +import { reactive } from "@odoo/owl"; + + +export const statisticsService = { + + start() { + const statistics = reactive({ isLoading: false }); + + const loadStatistics = async () => { + statistics.isLoading = true; + try { + const updated_value = await rpc("/awesome_dashboard/statistics"); + Object.assign(statistics, updated_value, {isLoading: false}); + } catch (error) { + console.error("Failed to load statistics:", error); + statistics.isLoading = false; + } + }; + + // Initial Load + loadStatistics(); + + // Auto-refresh every 10 seconds + setInterval(loadStatistics, 100000); + + + return statistics; + }, +}; + +registry + .category("services") + .add("awesome_dashboard.statistics", statisticsService); diff --git a/awesome_dashboard/static/src/dashboard_loader.js b/awesome_dashboard/static/src/dashboard_loader.js new file mode 100644 index 00000000000..a5bdc15e1e9 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_loader.js @@ -0,0 +1,13 @@ +import { registry } from "@web/core/registry"; +import { LazyComponent } from "@web/core/assets"; +import { Component, xml } from "@odoo/owl"; + +class AwesomeDashboardLoader extends Component { + static components = { LazyComponent }; + static template = xml` + + `; + +} + +registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboardLoader); diff --git a/awesome_owl/static/src/components/card/card.js b/awesome_owl/static/src/components/card/card.js new file mode 100644 index 00000000000..3e85928e555 --- /dev/null +++ b/awesome_owl/static/src/components/card/card.js @@ -0,0 +1,23 @@ +import { Component, useState } from "@odoo/owl"; + +export class Card extends Component { + static template = "awesome_owl.card"; + + static props = { + title: { type: String, optional: true }, + slots: { + type: Object, + optional: true, + }, + }; + + setup() { + this.state = useState({ + isCounterOpen: false, + }); + } + + toggleCounter() { + this.state.isCounterOpen = !this.state.isCounterOpen; + } +} diff --git a/awesome_owl/static/src/components/card/card.xml b/awesome_owl/static/src/components/card/card.xml new file mode 100644 index 00000000000..c1816078162 --- /dev/null +++ b/awesome_owl/static/src/components/card/card.xml @@ -0,0 +1,18 @@ + + + +
+
+
+ +
+ +
+ +
+
+
+
+
diff --git a/awesome_owl/static/src/components/counter/counter.js b/awesome_owl/static/src/components/counter/counter.js new file mode 100644 index 00000000000..1bb8a03a4cf --- /dev/null +++ b/awesome_owl/static/src/components/counter/counter.js @@ -0,0 +1,20 @@ +import { Component, useState } from "@odoo/owl"; + +export class Counter extends Component { + static template = "awesome_owl.counter"; + + static props = { + callbackIncrement: { type: Function, optional: true }, + }; + + setup() { + this.state = useState({ value: 0 }); + } + + increment() { + this.state.value += 1; + if (this.props.callbackIncrement) { + this.props.callbackIncrement(); + } + } +} diff --git a/awesome_owl/static/src/components/counter/counter.xml b/awesome_owl/static/src/components/counter/counter.xml new file mode 100644 index 00000000000..68c4ec1f15f --- /dev/null +++ b/awesome_owl/static/src/components/counter/counter.xml @@ -0,0 +1,13 @@ + + + + +
+

+ +

+ +
+
+
+
diff --git a/awesome_owl/static/src/components/todo/todo_item.js b/awesome_owl/static/src/components/todo/todo_item.js new file mode 100644 index 00000000000..b478e66bbfc --- /dev/null +++ b/awesome_owl/static/src/components/todo/todo_item.js @@ -0,0 +1,19 @@ +import { Component } from "@odoo/owl"; + +export class TodoItem extends Component { + static template = "awesome_owl.todo_item"; + + static props = { + todo_item: { type: Object }, + callbackToggleState: { type: Function }, + callbackRemoveTodo: { type: Function }, + }; + + removeTodo = (removeTodoId) => { + this.props.callbackRemoveTodo(removeTodoId); + }; + + toggleState = (todoId) => { + this.props.callbackToggleState(todoId); + }; +} diff --git a/awesome_owl/static/src/components/todo/todo_item.xml b/awesome_owl/static/src/components/todo/todo_item.xml new file mode 100644 index 00000000000..8a922365afc --- /dev/null +++ b/awesome_owl/static/src/components/todo/todo_item.xml @@ -0,0 +1,21 @@ + + + +
+
+ + + . + + + + +
+ +
+
+
diff --git a/awesome_owl/static/src/components/todo/todo_list.js b/awesome_owl/static/src/components/todo/todo_list.js new file mode 100644 index 00000000000..84d57842609 --- /dev/null +++ b/awesome_owl/static/src/components/todo/todo_list.js @@ -0,0 +1,49 @@ +import { Component, useState, useRef } from "@odoo/owl"; +import { TodoItem } from "./todo_item"; +import { useAutofocus } from "../../utils"; + +export class TodoList extends Component { + static template = "awesome_owl.todo_list"; + static components = { TodoItem }; + static props = {}; + + setup() { + this.todos = useState([]); + this.todoCounterId = 0; + + this.inputRef = useRef("inputRef"); + + useAutofocus(this.inputRef); + } + + addTodo(event) { + if (event.keyCode == 13) { + const newTask = event.target.value.trim(); + + if (newTask) { + this.todos.push({ + id: this.todoCounterId, + description: newTask, + isCompleted: false, + }); + this.todoCounterId++; + event.target.value = ""; + } + } + this.todos.push(); + } + + removeTodo = (removeTodoId) => { + const index = this.todos.findIndex((todo) => todo.id === removeTodoId); + if (index !== -1) { + this.todos.splice(index, 1); + } + }; + + toggleTodoState(todoId) { + const todo = this.todos.find((t) => t.id === todoId); + if (todo) { + todo.isCompleted = !todo.isCompleted; + } + } +} diff --git a/awesome_owl/static/src/components/todo/todo_list.xml b/awesome_owl/static/src/components/todo/todo_list.xml new file mode 100644 index 00000000000..aa190a7088c --- /dev/null +++ b/awesome_owl/static/src/components/todo/todo_list.xml @@ -0,0 +1,17 @@ + + + +
+

Todo List

+ + + + +
+
+
diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 657fb8b07bb..8a782a6c624 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,7 +1,22 @@ /** @odoo-module **/ -import { Component } from "@odoo/owl"; +import { Component, markup, useState } from "@odoo/owl"; +import { Counter } from "./components/counter/counter"; +import { Card } from "./components/card/card"; +import { TodoList } from "./components/todo/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.state = useState({ sum: 0 }); + } + + card1ContentValue = markup("
Some Content
"); + + incrementSum() { + this.state.sum += 1; + } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..f25370ccf37 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -1,10 +1,36 @@ - + - -
- hello world +
+ +

Counter

+
+ + +
+ + +
+

Total Sum: + +

+
+ + +

Cards

+
+ + + + + + +
+ + +
+ +
- diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js new file mode 100644 index 00000000000..7c0fceee1ee --- /dev/null +++ b/awesome_owl/static/src/utils.js @@ -0,0 +1,15 @@ +import { onMounted } from "@odoo/owl"; + +/** + * Automatically focuses on the provided reference element when the component is mounted. + * + * @param {Object} ref - The reference object to the DOM element. + * @returns {void} + */ +export const useAutofocus = (ref) => { + onMounted(() => { + if (ref?.el) { + ref.el.focus(); + } + }); +}; diff --git a/estate/__init__.py b/estate/__init__.py new file mode 100644 index 00000000000..720587b971a --- /dev/null +++ b/estate/__init__.py @@ -0,0 +1,4 @@ +from . import models +from . import controllers +from . import tests +from . import wizard diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 00000000000..377e313d96b --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,33 @@ +{ + 'name': 'Estate', + 'description': 'Estate Module', + 'version': '1.0', + 'depends': ['base','mail', 'website'], + 'author': 'Shiv Bhadaniya', + 'application': True, + 'installable': True, + 'license': 'LGPL-3', + 'category': 'Real Estate/Brokerage', + 'data': [ + 'security/estate_security.xml', + 'security/ir.model.access.csv', + 'data/estate.property.type.csv', + 'wizard/estate_property_offer_wizard_views.xml', + 'views/estate_property_views.xml', + 'report/estate_property_offer_subtemplate.xml', + 'report/estate_property_templates.xml', + 'report/estate_property_offer_res_user_template.xml', + 'report/estate_property_reports.xml', + 'views/estate_available_property_view.xml', + 'views/estate_property_tags_views.xml', + 'views/estate_property_offers_views.xml', + 'views/estate_property_types_views.xml', + 'views/estate_res_users_views.xml', + 'demo/estate_property_demo.xml', + 'demo/estate_property_website.xml', + 'views/estate_menus.xml', + ], + 'demo': [ + 'demo/estate_property_demo.xml', + ], +} diff --git a/estate/controllers/__init__.py b/estate/controllers/__init__.py new file mode 100644 index 00000000000..59d782585f3 --- /dev/null +++ b/estate/controllers/__init__.py @@ -0,0 +1 @@ +from . import estate_available_property \ No newline at end of file diff --git a/estate/controllers/estate_available_property.py b/estate/controllers/estate_available_property.py new file mode 100644 index 00000000000..70d0f5e7f86 --- /dev/null +++ b/estate/controllers/estate_available_property.py @@ -0,0 +1,50 @@ +from odoo import http +from odoo.http import request + +class EstateAvailableProperty(http.Controller): + + @http.route(['/estate/available_property', '/estate/available_property/page/'], type='http', auth='public', methods=['GET'], website=True) + def available_property(self, page=1, **kw): + """ + Render a list of available properties with pagination. + """ + Property = request.env['estate.property'] + + domain = [('state', 'in', ['new', 'offer_received'])] + listed_after = kw.get('listed_after') + if listed_after: + domain.append(('create_date', '>=', listed_after)) + + property_count = Property.search_count(domain) + url_args = {'listed_after': listed_after} if listed_after else {} + + pager = request.website.pager( + url="/estate/available_property", + total=property_count, + page=page, + step=6, + url_args=url_args + ) + + properties = Property.search(domain, limit=6, offset=pager['offset']) + + return request.render('estate.available_property_listing', { + 'properties': properties, + 'page_name': 'properties', + 'default_url': '/estate/available_property', + 'pager': pager, + 'listed_after': listed_after or False, + }) + + + @http.route('/estate/available_property_details/', type='http', auth='public', website=True) + def property_details(self, id, **kw): + Property = request.env['estate.property'] + property = Property.browse(id) + + if not property: + return request.not_found() + + return request.render('estate.available_property_details', { + 'property': property, + }) diff --git a/estate/data/estate.property.type.csv b/estate/data/estate.property.type.csv new file mode 100644 index 00000000000..15fab584b56 --- /dev/null +++ b/estate/data/estate.property.type.csv @@ -0,0 +1,5 @@ +"id","name" +estate_property_type_1,"Residential" +estate_property_type_2,"Commercial" +estate_property_type_3,"Industrial", +estate_property_type_4,"Land" diff --git a/estate/demo/estate_property_demo.xml b/estate/demo/estate_property_demo.xml new file mode 100644 index 00000000000..245eef85359 --- /dev/null +++ b/estate/demo/estate_property_demo.xml @@ -0,0 +1,276 @@ + + + + + + Big Villa + A nice and big villa + 12345 + new + True + 10 + 6 + 100 + 4 + True + True + 100000 + north + + + + + Luxury Mansion + An elegant and luxurious mansion + 54321 + new + True + 25 + 8 + 250 + 6 + True + True + 50000 + south + + + + + Modern Apartment + A stylish and modern apartment + 67890 + new + True + 15 + 3 + 90 + 2 + False + False + 0 + east + + + + + Countryside Cottage + A cozy cottage in the countryside + 13579 + new + True + 8 + 4 + 120 + 3 + True + True + 20000 + west + + + + + Skyline Penthouse + A penthouse with a breathtaking skyline view + 24680 + new + True + 30 + 5 + 180 + 2 + False + False + 0 + north + + + + + Seaside Bungalow + A relaxing bungalow by the sea + 11223 + new + True + 12 + 4 + 110 + 3 + True + True + 15000 + south + + + + + Mountain Retreat + A peaceful retreat in the mountains + 33445 + new + True + 20 + 6 + 160 + 4 + True + True + 40000 + west + + + + + Lake House + A beautiful house by the lake + 55667 + new + True + 18 + 5 + 140 + 3 + True + True + 30000 + east + + + + + Urban Loft + A modern loft in the city + 66778 + new + True + 22 + 2 + 85 + 1 + False + False + 0 + north + + + + + Desert Villa + A luxurious villa in the desert + 77889 + new + True + 35 + 7 + 200 + 5 + True + True + 70000 + south + + + + + Eco Cabin + A sustainable eco-friendly cabin + 88990 + new + True + 14 + 3 + 95 + 2 + False + True + 25000 + west + + + + + Cliffside Retreat + A serene retreat on the cliffs + 99001 + new + True + 28 + 4 + 150 + 3 + True + True + 35000 + north + + + + + Seaside Bungalow + A cozy bungalow by the sea + 10101 + new + True + 20 + 3 + 120 + 2 + True + True + 28000 + east + + + + + Hilltop Mansion + A grand mansion on the hills + 20202 + new + True + 50 + 8 + 300 + 6 + True + True + 80000 + south + + + + + Trailer home + Home in a tariler park + 54321 + True + new + 10 + 1 + 10 + 4 + False + True + 3 + south + + + + diff --git a/estate/demo/estate_property_demo_offers.xml b/estate/demo/estate_property_demo_offers.xml new file mode 100644 index 00000000000..6116ea3b036 --- /dev/null +++ b/estate/demo/estate_property_demo_offers.xml @@ -0,0 +1,27 @@ + + + + + + + 10000 + 14 + + + + + + + 1500000 + 14 + + + + + + + 1500001 + 14 + + + \ No newline at end of file diff --git a/estate/demo/estate_property_website.xml b/estate/demo/estate_property_website.xml new file mode 100644 index 00000000000..6999efd8c06 --- /dev/null +++ b/estate/demo/estate_property_website.xml @@ -0,0 +1,9 @@ + + + + + Properties + /estate/available_property + + + diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..41393ab1e39 --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1,5 @@ +from . import estate_property +from . import estate_property_offer +from . import estate_property_tag +from . import estate_property_type +from . import res_users \ No newline at end of file diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..8a19571d904 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,166 @@ +from datetime import datetime, timedelta +from odoo import api, fields, models +from odoo.exceptions import UserError, ValidationError +from odoo.tools.float_utils import float_compare, float_is_zero + +class EstateProperty(models.Model): + _name = "estate.property" + _description = "Estate Property Model" + _inherit = ["mail.thread"] + _order = "id desc" + + name = fields.Char(required=True, tracking=True) + description = fields.Text() + postcode = fields.Char() + date_availability = fields.Date(copy=False, default= datetime.now() + timedelta(days=90)) + 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([ + ('north', 'North'), + ('south', 'South'), + ('east', 'East'), + ('west', 'West'), + ]) + active = fields.Boolean(default=False) + state = fields.Selection([ + ('new', 'New'), + ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('canceled', 'Canceled'), + ], copy=False, default='new', required=True, tracking=True) + image = fields.Image(string="Property Image") + + # Many2one relationship + property_type_id = fields.Many2one('estate.property.type', string="Property Type") + buyer_id = fields.Many2one('res.partner', string="Buyer", copy=False) + user_id = fields.Many2one('res.users', string="Salesperson", default=lambda self: self.env.user) + + # Many2many relationship + tag_ids = fields.Many2many('estate.property.tag', string="Property Tags") + + # One2many relationship + offer_ids = fields.One2many('estate.property.offer', 'property_id', string="Offer") + + # Computed fields + total_area = fields.Integer(compute='_compute_total_area', store=True) + + best_price = fields.Float(compute='_compute_best_price', store=True) + + company_id = fields.Many2one('res.company', string="Company", default=lambda self: self.env.user.company_id) + + # ------------------------------------------------------------------------- + # COMPUTE METHODS + # ------------------------------------------------------------------------- + + @api.depends('living_area', 'garden_area') + def _compute_total_area(self): + for data in self: + data.total_area = data.living_area + data.garden_area + + @api.depends('offer_ids') + def _compute_best_price(self): + for data in self: + prices = data.offer_ids.mapped('price') + if prices: + data.best_price = max(prices) + else: + data.best_price = 0.0 + + + + # ------------------------------------------------------------------------- + # ONCHANGE METHODS + # ------------------------------------------------------------------------- + + @api.onchange('garden') + def _onchange_garden(self): + """ Set garden area to 10 and orientation to North when garden is True. + Reset them to empty when garden is False. + """ + if self.garden: + self.garden_area = 10 + self.garden_orientation = 'north' + else: + self.garden_area = 0 + self.garden_orientation = False + + + + # ------------------------------------------------------------ + # ACTIONS + # ------------------------------------------------------------ + + def action_property_sold(self): + if self.state == 'canceled': + raise UserError("You cannot sell a canceled property") + else: + self.state = 'sold' + + offers = self.offer_ids + any_offer_accept = False + for offer in offers: + if offer.status == 'accepted': + any_offer_accept = True + continue + if any_offer_accept == False: + raise ValidationError("At least one offer must be accepted before selling the property.") + + self.active = False + return True + + def action_property_cancel(self): + if self.state == 'sold': + raise UserError("You cannot cancel a sold property") + else: + self.state = 'canceled' + + self.active = False + return True + + + + # ------------------------------------------------------------------------- + # SQL CONSTRAINTS QUERIES + # ------------------------------------------------------------------------- + + _sql_constraints = [ + ('check_expected_price', 'CHECK(expected_price > 0)', 'The expected price must be positive'), + ] + + _sql_constraints = [ + ('check_selling_price', 'CHECK(selling_price > 0)', 'The selling price must be positive'), + ] + + + + # ------------------------------------------------------------------------- + # CONSTRAINTS METHODS + # ------------------------------------------------------------------------- + + @api.constrains('selling_price', 'expected_price') + def _check_selling_price(self): + for record in self: + if float_is_zero(record.selling_price, precision_digits=2): + continue + + minimum_price = record.expected_price * 0.9 + if float_compare(record.selling_price, minimum_price, precision_digits=2) == -1: + raise ValidationError(f"The selling price cannot be lower than 90% of the expected price.") + + + # ------------------------------------------------------------------------- + # CRUD METHODS + # ------------------------------------------------------------------------- + + @api.ondelete(at_uninstall=False) + def _unlink_if_not_new_or_cancelled(self): + for record in self: + if record.state not in ('new', 'canceled'): + raise UserError("You may only delete properties in state 'New' or 'Canceled'") diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..c29fa66344e --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,99 @@ +from datetime import datetime, timedelta +from odoo import api, fields, models +from odoo.exceptions import UserError + +class EstatePropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "Estate Property Offer" + + _order = "price desc" + price = fields.Float(string="Offer Price") + status = fields.Selection([ + ('accepted', 'Accepted'), + ('refused', 'Refused'), + ], string="Offer Status", 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(string="Validity (in days)", default=7) + date_deadline = fields.Date(string="Deadline", compute="_compute_date_deadline", inverse="_inverse_validity", store=True) + create_date = fields.Date(readonly=True, default=fields.Date.today) + property_type_id = fields.Many2one('estate.property.type', string="Property Type", related='property_id.property_type_id', store=True) # related field: Automatically fetches the property type. + + + # ------------------------------------------------------------------------- + # COMPUTE METHODS + # ------------------------------------------------------------------------- + + @api.depends('validity') + def _compute_date_deadline(self): + for record in self: + if record.validity is not None: + record.date_deadline = datetime.now().date() + timedelta(days=record.validity) + else: + # If validity is not set, set the deadline to 7 days + record.date_deadline = datetime.now().date() + timedelta(days=7) + + def _inverse_validity(self): + for record in self: + if record.create_date and record.date_deadline: + record.validity = (record.date_deadline - record.create_date).days + + + + # ------------------------------------------------------------ + # ACTIONS + # ------------------------------------------------------------ + + def action_offer_accept(self): + for record in self: + record.status = "accepted" + record.property_id.state = "offer_accepted" + record.property_id.selling_price = record.price + record.property_id.buyer_id = record.partner_id + + other_offers = self.env['estate.property.offer'].search([ + ('property_id', '=', record.property_id.id), + ('id', '!=', record.id), + ('status', '!=', 'refused') + ]) + other_offers.write({'status': 'refused'}) + return True + + def action_offer_refuse(self): + self.status = "refused" + return True + + + # ------------------------------------------------------------------------- + # SQL CONSTRAINTS QUERIES + # ------------------------------------------------------------------------- + + _sql_constraints = [ + ('check_price', 'CHECK(price > 0)', 'The price must be positive'), + ] + + # ------------------------------------------------------------------------- + # CRUD METHODS + # ------------------------------------------------------------------------- + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + + # Sold Property can't make a new offer + property = self.env['estate.property'].browse(vals.get('property_id')) + if not property.exists(): + raise UserError("The property you are referring to doesn't exist.") + elif property.state == 'sold': + raise UserError("You cannot create an offer for a sold property.") + + offer_price = vals.get('price') + current_maximum_offer = self.search([('property_id', '=', vals['property_id'])], order="price desc", limit=1) # Fetch the current maximum offer for the property, already stored in the descending order of price. + + if offer_price < current_maximum_offer.price: + raise UserError(f"The offer price must be higher than {current_maximum_offer.price}") + else: + offer_received_property = self.env['estate.property'].browse(vals.get('property_id')) + offer_received_property.state = "offer_received" + + return super().create(vals_list) diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..c25ec2e1da0 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,19 @@ +from odoo import fields, models + +class EstatePropertyTag(models.Model): + _name = "estate.property.tag" + _description = "Estate Property Tags" + + _order = "name" + + name = fields.Char(string="Tag Name") + + color = fields.Integer() + + # ------------------------------------------------------------------------- + # SQL CONSTRAINTS QUERIES + # ------------------------------------------------------------------------- + + _sql_constraints = [ + ('unique_tag_name', 'UNIQUE(name)', 'The property tag name must be unique.') + ] diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..5a5365627fc --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,32 @@ +from odoo import api, fields, models + +class EstatePropertyType(models.Model): + _name = "estate.property.type" + _description = "Estate Property Type" + _order = "name" + + name = fields.Char(string="Type Name") + property_ids = fields.One2many('estate.property', 'property_type_id', string="Properties") + + offer_ids = fields.One2many('estate.property.offer', 'property_type_id', string="Offers") + offer_count = fields.Integer(compute='_compute_offer_count') + sequence = fields.Integer() + + + # ------------------------------------------------------------------------- + # SQL CONSTRAINTS QUERIES + # ------------------------------------------------------------------------- + + _sql_constraints = [ + ('unique_type_name', 'UNIQUE(name)', 'The property type name must be unique.') + ] + + + # ------------------------------------------------------------------------- + # COMPUTE METHODS + # ------------------------------------------------------------------------- + + @api.depends('offer_ids') + def _compute_offer_count(self): + for record in self: + record.offer_count = len(record.offer_ids) diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..cd5454b1aab --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,7 @@ +from odoo import fields, models + +class ResUsers(models.Model): + _name = 'res.users' + _inherit = 'res.users' + + property_ids = fields.One2many('estate.property', 'user_id', string='Properties', domain = [("state" , "in" , ["new" , "offer_received"])]) diff --git a/estate/report/estate_property_offer_res_user_template.xml b/estate/report/estate_property_offer_res_user_template.xml new file mode 100644 index 00000000000..e83a7da6500 --- /dev/null +++ b/estate/report/estate_property_offer_res_user_template.xml @@ -0,0 +1,38 @@ + + + + + + diff --git a/estate/report/estate_property_offer_subtemplate.xml b/estate/report/estate_property_offer_subtemplate.xml new file mode 100644 index 00000000000..ffa1f8e3f22 --- /dev/null +++ b/estate/report/estate_property_offer_subtemplate.xml @@ -0,0 +1,45 @@ + + + + diff --git a/estate/report/estate_property_reports.xml b/estate/report/estate_property_reports.xml new file mode 100644 index 00000000000..5131a111b6d --- /dev/null +++ b/estate/report/estate_property_reports.xml @@ -0,0 +1,25 @@ + + + + + Print + estate.property + estate.report_property_offers + estate.report_property_offers + '%s_offers' % (object.name or 'Property') + + report + qweb-pdf + + + + + Report + res.users + estate.estate_property_offer_res_user_template + estate.estate_property_offer_res_user_template + + report + qweb-pdf + + diff --git a/estate/report/estate_property_templates.xml b/estate/report/estate_property_templates.xml new file mode 100644 index 00000000000..c820f5a0a22 --- /dev/null +++ b/estate/report/estate_property_templates.xml @@ -0,0 +1,27 @@ + + + + diff --git a/estate/security/estate_security.xml b/estate/security/estate_security.xml new file mode 100644 index 00000000000..a4f944dbf41 --- /dev/null +++ b/estate/security/estate_security.xml @@ -0,0 +1,29 @@ + + + + Agent + + + + + Manager + + + + + + + + + ['|', ('user_id', '=', user.id), ('user_id', '=', False),] + + + + + + + + + + + diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..7c2482dd165 --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,11 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 +access_estate_property_offer_wizard,access_estate_property_offer_wizard,estate.model_estate_property_offer_wizard,base.group_user,1,1,1,1 +access_estate_property_manager,access_estate_property_manager,estate.model_estate_property,estate.estate_group_manager,1,1,1,0 +access_estate_property_type_manager,access_estate_property_type_manager,estate.model_estate_property_type,estate.estate_group_manager,1,1,1,1 +access_estate_property_tag_manager,access_estate_property_tag_manager,estate.model_estate_property_tag,estate.estate_group_manager,1,1,1,1 +access_estate_property_offer_manager,access_estate_property_offer_manager,estate.model_estate_property_offer,estate.estate_group_manager,1,1,1,1 +access_estate_property_type_agent,access_estate_property_type_agent,estate.model_estate_property_type,estate.estate_group_user,1,0,0,0 +access_estate_property_tag_agent,access_estate_property_tag_agent,estate.model_estate_property_tag,estate.estate_group_user,1,0,0,0 +access_estate_property_agent,access_estate_property_agent,estate.model_estate_property,estate.estate_group_user,1,1,1,0 +access_estate_property_offer_agent,access_estate_property_offer_agent,estate.model_estate_property_offer,estate.estate_group_user,1,1,1,0 diff --git a/estate/static/description/icon.png b/estate/static/description/icon.png new file mode 100644 index 00000000000..cf7dd4d86ce Binary files /dev/null and b/estate/static/description/icon.png differ diff --git a/estate/tests/__init__.py b/estate/tests/__init__.py new file mode 100644 index 00000000000..a42df2e12ba --- /dev/null +++ b/estate/tests/__init__.py @@ -0,0 +1 @@ +from . import test_property_sold diff --git a/estate/tests/test_property_sold.py b/estate/tests/test_property_sold.py new file mode 100644 index 00000000000..15c265090b9 --- /dev/null +++ b/estate/tests/test_property_sold.py @@ -0,0 +1,18 @@ +from odoo.tests.common import TransactionCase +from odoo.exceptions import ValidationError +from odoo.tests import tagged + +@tagged('post_install', '-at_install') +class TestEstateProperty(TransactionCase): + + def test_sell_property_without_accepted_offer(self): + estate_property = self.env['estate.property'] + + property = estate_property.create({ + "name": "Test Property Without Offer", + "expected_price": "100", + "state": "new", + }) + + with self.assertRaises(ValidationError): + property.action_property_sold() diff --git a/estate/views/estate_available_property_view.xml b/estate/views/estate_available_property_view.xml new file mode 100644 index 00000000000..89ff3d97249 --- /dev/null +++ b/estate/views/estate_available_property_view.xml @@ -0,0 +1,120 @@ + + + + + + + + diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..8cee18839dd --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/estate/views/estate_property_offers_views.xml b/estate/views/estate_property_offers_views.xml new file mode 100644 index 00000000000..4eca9ff6928 --- /dev/null +++ b/estate/views/estate_property_offers_views.xml @@ -0,0 +1,25 @@ + + + + + Property Offer + estate.property.offer + list,form + + + + estate.property.offer.list + estate.property.offer + + + + + + + +
+

+ +

+ + + + + + + + + + + + + + + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..5d944c23009 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,162 @@ + + + + Estate + estate.property + list,form,kanban + {'search_default_available_property': 1} + + + + estate.property.list + estate.property + + + + + + + + + + + +
+
+
+
+
+ + + estate.property.form + estate.property + +
+
+
+ +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+ + + estate.property.kanban + estate.property + + + + + +
+ +
+
Expected Price: +
+
Best Price: +
+
Selling Price: +
+
+ +
+
+
+
+
+
+ + + estate.property.search + estate.property + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/estate/views/estate_res_users_views.xml b/estate/views/estate_res_users_views.xml new file mode 100644 index 00000000000..8854f841128 --- /dev/null +++ b/estate/views/estate_res_users_views.xml @@ -0,0 +1,15 @@ + + + + res.users.view.form.inherit.estate + res.users + + + + + + + + + + diff --git a/estate/wizard/__init__.py b/estate/wizard/__init__.py new file mode 100644 index 00000000000..e9926bcd3ec --- /dev/null +++ b/estate/wizard/__init__.py @@ -0,0 +1 @@ +from . import estate_property_offer_wizard diff --git a/estate/wizard/estate_property_offer_wizard.py b/estate/wizard/estate_property_offer_wizard.py new file mode 100644 index 00000000000..cb8cedb209c --- /dev/null +++ b/estate/wizard/estate_property_offer_wizard.py @@ -0,0 +1,34 @@ +from datetime import timedelta + +from odoo import fields, models +from odoo.exceptions import UserError + +class EstatePropertyOfferWizard(models.TransientModel): + _name = "estate.property.offer.wizard" + _description = "Estate Property Offer Wizard" + + price = fields.Float(string="Price", required=True) + buyer_id = fields.Many2one("res.partner", string="Partner", required=True) + date_deadline = fields.Date("Deadline", default=lambda self: fields.Date.today() + timedelta(days=7), required=True) + + + + def add_offers_to_multiple_properties(self): + selected_properties = self.env['estate.property'].browse(self.env.context.get('active_ids', [])) + + offer_price = self.price + offer_buyer_id = self.buyer_id + offer_date_deadline = self.date_deadline + + for property in selected_properties: + + if property.state == 'sold': + raise UserError(f"The property '{property.name}' is alredy sold ") + + self.env['estate.property.offer'].create({ + 'price' : offer_price, + 'partner_id' : offer_buyer_id.id, + 'date_deadline':offer_date_deadline, + 'property_id' : property.id + }) + return {'type': 'ir.actions.act_window_close'} diff --git a/estate/wizard/estate_property_offer_wizard_views.xml b/estate/wizard/estate_property_offer_wizard_views.xml new file mode 100644 index 00000000000..8773fa7b90d --- /dev/null +++ b/estate/wizard/estate_property_offer_wizard_views.xml @@ -0,0 +1,29 @@ + + + + estate.property.offer.wizard.form + estate.property.offer.wizard + +
+ + + + + +
+
+
+
+
+ + + Estate Property Offer Wizard + estate.property.offer.wizard + form + new + + + +
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..29cf8d5dcc6 --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,14 @@ +{ + 'name': 'estate_account', + 'description': 'Estate Account Module', + 'sequence': 1, + 'version': '1.0', + 'depends': ['estate', 'account'], + 'author': 'Shiv Bhadaniya', + "installable": True, + "application": True, + 'license': 'LGPL-3', + 'data': [ + 'report/estate_property_inherit.xml', + ] +} 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..0ba4bfa3468 --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,32 @@ +from odoo import Command, models + +class EstateAccount(models.Model): + _inherit = "estate.property" + + def action_property_sold(self): + + self.check_access('write') + + self.env["account.move"].sudo().create( + { + "move_type": "out_invoice", + "partner_id": self.buyer_id.id, + "invoice_line_ids": [ + Command.create( + { + "name": "Property Sale", + "quantity": 1, + "price_unit": 1.06 * self.selling_price, + } + ), + Command.create( + { + "name": "Additional Charges", + "quantity": 1, + "price_unit": self.selling_price + 100, + } + ), + ], + } + ) + return super().action_property_sold() diff --git a/estate_account/report/estate_property_inherit.xml b/estate_account/report/estate_property_inherit.xml new file mode 100644 index 00000000000..bd3712546a2 --- /dev/null +++ b/estate_account/report/estate_property_inherit.xml @@ -0,0 +1,17 @@ + + + + + + +