diff --git a/.gitignore b/.gitignore
index b6e47617de1..fd710c264d3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -102,6 +102,7 @@ celerybeat.pid
*.sage.py
# Environments
+.vscode/
.env
.venv
env/
diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py
index 31406e8addb..35cb019c772 100644
--- a/awesome_dashboard/__manifest__.py
+++ b/awesome_dashboard/__manifest__.py
@@ -1,30 +1,29 @@
# -*- 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/**/*",
+ ],
+ "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/chart/pie.js b/awesome_dashboard/static/src/dashboard/chart/pie.js
new file mode 100644
index 00000000000..4bd0d2f5601
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/chart/pie.js
@@ -0,0 +1,40 @@
+import { Component, useRef, onWillStart, useEffect } from "@odoo/owl";
+import { loadJS } from "@web/core/assets";
+
+export class PieChart extends Component {
+ static template = "awesome_dashboard.pie_chart";
+ static props = {
+ data: { type: Object },
+ };
+
+ setup() {
+ this.canvasRef = useRef('canvasRef');
+ this.chart = null;
+
+ onWillStart(async () => {
+ await loadJS(["/web/static/lib/Chart/Chart.js"]);
+ });
+
+ useEffect(()=> this.renderChart())
+
+ }
+
+ renderChart() {
+ if (this.chart) {
+ this.chart.destroy();}
+ this.chart = new Chart(this.canvasRef.el, this.getChartConfig());
+ }
+
+ getChartConfig() {
+ return {
+ type: 'pie',
+ data: {
+ labels: this.props.labels,
+ datasets: [{
+ data: Object.values(this.props.data)
+ }],
+ },
+ }
+ }
+
+}
diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard/chart/pie.xml
similarity index 51%
rename from awesome_dashboard/static/src/dashboard.xml
rename to awesome_dashboard/static/src/dashboard/chart/pie.xml
index 1a2ac9a2fed..c5447fff565 100644
--- a/awesome_dashboard/static/src/dashboard.xml
+++ b/awesome_dashboard/static/src/dashboard/chart/pie.xml
@@ -1,8 +1,9 @@
-
- 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..6b8315016e2
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard.js
@@ -0,0 +1,56 @@
+/** @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 "./dashboarditem/dashboarditem";
+import { PieChart } from "./chart/pie";
+import { dashboardRegistry } from "./dashboarditem/dashboard_items";
+import { SettingsDialog } from "./dialog/dialog";
+
+class AwesomeDashboard extends Component {
+ static template = "awesome_dashboard.AwesomeDashboard";
+ static components = { Layout , DashboardItem , PieChart};
+ setup() {
+ this.action = useService("action");
+ this.dialogService = useService("dialog");
+
+ this.statisticsService = useService("awesome_dashboard.statistics");
+ this.state = useState(this.statisticsService.state.dashboardItems);
+
+ const savedHiddenItems = JSON.parse(localStorage.getItem("hidden_dashboard_items"));
+ this.state.hiddenItems = new Set(savedHiddenItems);
+
+
+ }
+ get display() {
+ return {controlPanel: {} }
+ }
+ async openCustomers() {
+
+ this.action.doAction("base.action_partner_form");
+ }
+
+ async opendialog() {
+ this.dialogService.add(SettingsDialog, {
+ onApply: (hiddenItems) => {
+ this.state.hiddenItems = new Set(hiddenItems);
+ },
+ });
+ }
+ async openCrmlead() {
+ this.action.doAction({
+ type: 'ir.actions.act_window',
+ name:'crm leads',
+ target: 'current',
+ res_model: 'crm.lead',
+ views: [[false, 'list'],[false, 'form']],
+ });
+ }
+ get visibleItems() {
+ return Object.values(dashboardRegistry.getAll()).filter(item => !this.state.hiddenItems.has(item.id));
+ }
+}
+
+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..e69de29bb2d
diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml
new file mode 100644
index 00000000000..7c32bf3c842
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard/dashboarditem/dashboard_items.js b/awesome_dashboard/static/src/dashboard/dashboarditem/dashboard_items.js
new file mode 100644
index 00000000000..2d33d48d056
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboarditem/dashboard_items.js
@@ -0,0 +1,78 @@
+/** @odoo-module **/
+import { NumberCard } from "./number_card";
+import { PieChartCard } from "./pie_chart_card";
+import { registry } from "@web/core/registry";
+
+export const dashboardRegistry = registry.category("awesome_dashboard.items");
+
+dashboardRegistry.add(
+ "average_quantity", {
+ id: "average_quantity",
+ description: "Average amount of t-shirt by order",
+ Component: NumberCard,
+ size: 1,
+ props: (data) => ({
+ title: "T-shirt Ordered",
+ value: data,
+ }),
+})
+
+dashboardRegistry.add(
+ "average_time", {
+ id: "average_time",
+ description: "Average Time",
+ Component: NumberCard,
+ size: 2,
+ props: (data) => ({
+ title: "Average time for an order to go from 'new' to 'sent' or 'cancelled'",
+ value: data,
+ }),
+})
+
+dashboardRegistry.add(
+ "nb_new_orders", {
+ id: "nb_new_orders",
+ description: "New orders this month",
+ Component: NumberCard,
+ size: 1,
+ props: (data) => ({
+ title: "Numbers of new orders this month",
+ value: data,
+ }),
+})
+
+dashboardRegistry.add(
+ "nb_cancelled_orders", {
+ id: "nb_cancelled_orders",
+ description: "Cancelled orders this month",
+ Component: NumberCard,
+ size: 1,
+ props: (data) => ({
+ title: "Numbers of cancelled orders this month",
+ value: data,
+ })
+})
+
+dashboardRegistry.add(
+ "total_amount", {
+ id: "total_amount",
+ description: "Total Orders",
+ Component: NumberCard,
+ size: 1,
+ props: (data) => ({
+ title: "Total Numbers of new orders this month",
+ value: data,
+ }),
+})
+
+dashboardRegistry.add(
+ "orders_by_size", {
+ id: "orders_by_size",
+ description: "Orders by size",
+ Component: PieChartCard,
+ size: 1.5,
+ props: (data) => ({
+ label: "Orders by size",
+ data: data,
+ }),
+})
\ No newline at end of file
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..854078553b4
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboarditem/dashboarditem.js
@@ -0,0 +1,9 @@
+import { Component } from "@odoo/owl";
+
+export class DashboardItem extends Component {
+ static template = "awesome_dashboard.dashboarditem";
+ static props ={
+ size: { type: Number, optional: true, default: 1},
+ }
+
+}
\ No newline at end of file
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..706c1076589
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboarditem/dashboarditem.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/awesome_dashboard/static/src/dashboard/dashboarditem/number_card.js b/awesome_dashboard/static/src/dashboard/dashboarditem/number_card.js
new file mode 100644
index 00000000000..221ba45cebc
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboarditem/number_card.js
@@ -0,0 +1,9 @@
+import { Component} from "@odoo/owl";
+
+export class NumberCard extends Component {
+ static template = "awesome_dashboard.NumberCard"
+ static props = {
+ title: String,
+ value: Number
+ }
+}
\ No newline at end of file
diff --git a/awesome_dashboard/static/src/dashboard/dashboarditem/number_card.xml b/awesome_dashboard/static/src/dashboard/dashboarditem/number_card.xml
new file mode 100644
index 00000000000..be7bc241310
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboarditem/number_card.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/awesome_dashboard/static/src/dashboard/dashboarditem/pie_chart_card.js b/awesome_dashboard/static/src/dashboard/dashboarditem/pie_chart_card.js
new file mode 100644
index 00000000000..9459decabd9
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboarditem/pie_chart_card.js
@@ -0,0 +1,12 @@
+import { Component} from "@odoo/owl";
+import { PieChart } from "../chart/pie";
+export class PieChartCard extends Component {
+ static template= 'awesome_dashboard.PieChartCard'
+
+ static props = {
+ label: String,
+ data: Object,
+ };
+
+ static components = { PieChart };
+}
\ No newline at end of file
diff --git a/awesome_dashboard/static/src/dashboard/dashboarditem/pie_chart_card.xml b/awesome_dashboard/static/src/dashboard/dashboarditem/pie_chart_card.xml
new file mode 100644
index 00000000000..48d423010c0
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboarditem/pie_chart_card.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/awesome_dashboard/static/src/dashboard/dialog/dialog.js b/awesome_dashboard/static/src/dashboard/dialog/dialog.js
new file mode 100644
index 00000000000..8bf91b59039
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dialog/dialog.js
@@ -0,0 +1,36 @@
+/** @odoo-module **/
+import { Component, useState } from "@odoo/owl";
+import { useService } from "@web/core/utils/hooks";
+import { Dialog } from "@web/core/dialog/dialog";
+import { dashboardRegistry } from "../dashboarditem/dashboard_items";
+
+export class SettingsDialog extends Component {
+
+ static template = "awesome_dashboard.SettingsDialog";
+ static components = { Dialog };
+ setup() {
+ this.dialogService = useService("dialog");
+ this.items = Object.values(dashboardRegistry.getAll());
+
+ const savedHiddenItems = JSON.parse(localStorage.getItem("hidden_dashboard_items"));
+ this.state = useState({
+ hiddenItems: new Set(savedHiddenItems),
+ });
+ }
+
+ toggleItem(event) {
+ if (this.state.hiddenItems.has(event.target.id)) {
+ this.state.hiddenItems.delete(event.target.id);
+ } else {
+ this.state.hiddenItems.add(event.target.id);
+ }
+ }
+
+ applySettings() {
+ localStorage.setItem("hidden_dashboard_items", JSON.stringify([...this.state.hiddenItems]));
+ this.props.onApply([...this.state.hiddenItems]);
+
+ this.dialogService.closeAll();
+ }
+}
+
diff --git a/awesome_dashboard/static/src/dashboard/dialog/dialog.xml b/awesome_dashboard/static/src/dashboard/dialog/dialog.xml
new file mode 100644
index 00000000000..7d1e75bba63
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dialog/dialog.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/awesome_dashboard/static/src/dashboard/service/awesome_dashboard_statistics.js b/awesome_dashboard/static/src/dashboard/service/awesome_dashboard_statistics.js
new file mode 100644
index 00000000000..2b02e88412b
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/service/awesome_dashboard_statistics.js
@@ -0,0 +1,31 @@
+/** @odoo-module **/
+import { registry } from "@web/core/registry";
+import { reactive } from "@odoo/owl";
+import { rpc } from "@web/core/network/rpc";
+
+const REFRESH_INTERVAL = 10000;
+
+export const statisticsService = {
+ dependencies: [],
+ async start(env) {
+ const state = reactive({ dashboardItems: {} });
+
+ async function loadStatistics() {
+ const data = await rpc("/awesome_dashboard/statistics", {});
+ Object.assign(state.dashboardItems, data); // Update in place
+ }
+
+ // Initial load
+ await loadStatistics();
+
+ // Periodic refresh
+ setInterval(loadStatistics, REFRESH_INTERVAL);
+
+ return {
+ state,
+ };
+ },
+};
+
+// Register the service
+registry.category("services").add("awesome_dashboard.statistics", statisticsService);
\ No newline at end of file
diff --git a/awesome_dashboard/static/src/dashboard_loader.js b/awesome_dashboard/static/src/dashboard_loader.js
new file mode 100644
index 00000000000..7e997778ba5
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard_loader.js
@@ -0,0 +1,13 @@
+import { Component ,xml } from "@odoo/owl";
+import { registry } from "@web/core/registry";
+import { LazyComponent } from "@web/core/assets";
+
+
+export class DashboardLoader extends Component {
+ static components = { LazyComponent };
+ static template = xml`
+
+ `;
+}
+
+registry.category("actions").add("awesome_dashboard.dashboard", DashboardLoader);
\ No newline at end of file
diff --git a/awesome_dashboard/views/views.xml b/awesome_dashboard/views/views.xml
index 47fb2b6f258..f0341cd364c 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..5e9759dcb7b
--- /dev/null
+++ b/awesome_owl/static/src/card/card.js
@@ -0,0 +1,14 @@
+/** @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},
+ slots: {type:Object}
+ }
+
+
+}
diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml
new file mode 100644
index 00000000000..672fa8b577a
--- /dev/null
+++ b/awesome_owl/static/src/card/card.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js
new file mode 100644
index 00000000000..f1035547286
--- /dev/null
+++ b/awesome_owl/static/src/counter/counter.js
@@ -0,0 +1,20 @@
+/** @odoo-module **/
+
+import { Component, useState } from "@odoo/owl";
+
+export class Counter extends Component {
+ static template = "awesome_owl.counter";
+ static props ={
+ incrementsum:{type:Function}
+ }
+
+ setup() {
+ this.state = useState({ value: 0 });
+ }
+
+
+ increment() {
+ this.state.value++;
+ this.props.incrementsum();
+ }
+}
diff --git a/awesome_owl/static/src/counter/counter.xml b/awesome_owl/static/src/counter/counter.xml
new file mode 100644
index 00000000000..07965335c23
--- /dev/null
+++ b/awesome_owl/static/src/counter/counter.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
Counter:
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js
index 657fb8b07bb..44637bb4cf4 100644
--- a/awesome_owl/static/src/playground.js
+++ b/awesome_owl/static/src/playground.js
@@ -1,7 +1,32 @@
/** @odoo-module **/
-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/todo_list";
export class Playground extends Component {
static template = "awesome_owl.playground";
+ static props ={
+
+ }
+ value2 = markup("
some text 2
");
+ static components = { Counter , Card , Todolist};
+
+ state= useState({
+ sum:0,
+ card1:false,
+ card2:false
+ })
+ incrementsum= ()=> {
+ this.state.sum++;
+ }
+ inccard1= ()=> {
+ this.state.card1= !this.state.card1;
+
+ }
+ inccard2= ()=> {
+ this.state.card2= !this.state.card2;
+
+ }
}
diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml
index 4fb905d59f9..f6b70094694 100644
--- a/awesome_owl/static/src/playground.xml
+++ b/awesome_owl/static/src/playground.xml
@@ -2,9 +2,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..01f0be104a7
--- /dev/null
+++ b/awesome_owl/static/src/todo/todo_item.js
@@ -0,0 +1,17 @@
+/** @odoo-module **/
+
+import { Component } from "@odoo/owl";
+
+export class Todoitem extends Component {
+ static template = "awesome_owl.todoitem";
+ static props = {
+ item: {type: Object
+ },
+ delete: {type: Function
+ }
+ }
+ check = () =>{
+ this.props.item.isCompleted = !this.props.item.isCompleted
+ }
+
+}
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..9979ee5dfd6
--- /dev/null
+++ b/awesome_owl/static/src/todo/todo_item.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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..922a318bc17
--- /dev/null
+++ b/awesome_owl/static/src/todo/todo_list.js
@@ -0,0 +1,54 @@
+/** @odoo-module **/
+
+import { Component, useState, onMounted, useRef } from "@odoo/owl";
+import { Todoitem } from "./todo_item";
+export class Todolist extends Component {
+ static template = "awesome_owl.todolist";
+ static props ={
+
+ }
+ setup() {
+ this.inputRef = useRef('input');
+ onMounted(() => {
+ this.inputRef.el.focus()
+ });
+ this.todo= useState([{
+ id:1,
+ name:"ch1-owl",
+ isCompleted: false
+ },
+ {
+ id:2,
+ name:"ch2-owl",
+ isCompleted: false
+ },
+ {
+ id:3,
+ name:"estate",
+ isCompleted: true
+ }])
+ }
+
+ addtodo = (e) => {
+ if(e.key=="Enter" && e.target.value){
+ this.todo.push({
+ id: this.todo.length +2,
+ name: e.target.value,
+ isCompleted: false
+
+ })
+ e.target.value=""
+
+ }
+ }
+
+ delete=(e)=>{
+ const index = this.todo.findIndex((todo) => todo.id === e.id);
+ if (index >= 0) {
+ this.todo.splice(index, 1);
+ }
+ }
+
+ static components = {Todoitem}
+
+}
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..221ae0b4648
--- /dev/null
+++ b/awesome_owl/static/src/todo/todo_list.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/book_price/__init__.py b/book_price/__init__.py
new file mode 100644
index 00000000000..0650744f6bc
--- /dev/null
+++ b/book_price/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/book_price/__manifest__.py b/book_price/__manifest__.py
new file mode 100644
index 00000000000..1f345594551
--- /dev/null
+++ b/book_price/__manifest__.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+{
+ "name": "book_price",
+ "summary": "Add Pricelist Price (yame)",
+ "description": "Add Pricelist Price (yame)",
+ "author": "Odoo",
+ "website": "https://www.odoo.com",
+ "category": "Tutorials/book_price",
+ "version": "0.1",
+ # any module necessary for this one to work correctly
+ "depends": ["base", "web", "sale", "sale_management", "account"],
+ "application": True,
+ "installable": True,
+ "data": [
+ "views/sale_order_views.xml",
+ "views/account_move_views.xml",
+ ],
+ "license": "AGPL-3",
+}
diff --git a/book_price/models/__init__.py b/book_price/models/__init__.py
new file mode 100644
index 00000000000..ea3d9579546
--- /dev/null
+++ b/book_price/models/__init__.py
@@ -0,0 +1,2 @@
+from . import sale_order_line
+from . import account_move_line
diff --git a/book_price/models/account_move_line.py b/book_price/models/account_move_line.py
new file mode 100644
index 00000000000..8f3ac000322
--- /dev/null
+++ b/book_price/models/account_move_line.py
@@ -0,0 +1,15 @@
+from odoo import api, fields, models
+
+
+class AccountMoveLine(models.Model):
+ _inherit = "account.move.line"
+
+ book_price = fields.Float(string="Book Price", compute="_compute_book_price")
+
+ @api.depends("product_id.lst_price")
+ def _compute_book_price(self):
+ print(" Book Price ".center(100, "="))
+ for record in self:
+ print(record.move_type)
+ print(record.product_id.lst_price)
+ record.book_price = record.product_id.lst_price
diff --git a/book_price/models/sale_order_line.py b/book_price/models/sale_order_line.py
new file mode 100644
index 00000000000..262ffd4be1c
--- /dev/null
+++ b/book_price/models/sale_order_line.py
@@ -0,0 +1,14 @@
+from odoo import api, fields, models
+
+
+class SaleOrderLine(models.Model):
+ _inherit = "sale.order.line"
+
+ book_price = fields.Float(
+ string="Book Price", readonly=True, compute="_compute_book_price"
+ )
+
+ @api.depends("product_id.lst_price")
+ def _compute_book_price(self):
+ for record in self:
+ record.book_price = record.product_id.lst_price
diff --git a/book_price/views/account_move_views.xml b/book_price/views/account_move_views.xml
new file mode 100644
index 00000000000..f838dd556b1
--- /dev/null
+++ b/book_price/views/account_move_views.xml
@@ -0,0 +1,20 @@
+
+
+
+
+ account.move.form
+ account.move
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/book_price/views/sale_order_views.xml b/book_price/views/sale_order_views.xml
new file mode 100644
index 00000000000..3d1ff67dc17
--- /dev/null
+++ b/book_price/views/sale_order_views.xml
@@ -0,0 +1,17 @@
+
+
+
+
+ sale.order.form
+ sale.order
+
+
+
+
+
+
+
+
+
+
+
diff --git a/budget/__init__.py b/budget/__init__.py
new file mode 100644
index 00000000000..c536983e2b2
--- /dev/null
+++ b/budget/__init__.py
@@ -0,0 +1,2 @@
+from . import models
+from . import wizard
\ No newline at end of file
diff --git a/budget/__manifest__.py b/budget/__manifest__.py
new file mode 100644
index 00000000000..f5020da3ea4
--- /dev/null
+++ b/budget/__manifest__.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+{
+ "name": "budget",
+ "summary": "budget (yame)",
+ "description": "budget (yame)",
+ "author": "Odoo",
+ "website": "https://www.odoo.com",
+ "category": "Tutorials/budget",
+ "version": "0.1",
+ "depends": ["base", "web", "account"],
+ "application": True,
+ "installable": True,
+ "data": [
+ "security/ir.model.access.csv",
+ "views/account_analytic_line.xml",
+ "wizard/budget_wizard_view.xml",
+ "views/budget_budget_views.xml",
+ "views/budget_line_views.xml",
+ "views/budget_menu.xml",
+ ],
+ "license": "AGPL-3",
+}
diff --git a/budget/models/__init__.py b/budget/models/__init__.py
new file mode 100644
index 00000000000..ba4fc70ff89
--- /dev/null
+++ b/budget/models/__init__.py
@@ -0,0 +1,3 @@
+from . import account_analytical_line
+from . import budget
+from . import budget_line
diff --git a/budget/models/account_analytical_line.py b/budget/models/account_analytical_line.py
new file mode 100644
index 00000000000..03200f57dd5
--- /dev/null
+++ b/budget/models/account_analytical_line.py
@@ -0,0 +1,7 @@
+from odoo import fields, models
+
+
+class AccountAnalyticLine(models.Model):
+ _inherit = "account.analytic.line"
+
+ budget_line_id = fields.Many2one("budget.line", ondelete="cascade")
diff --git a/budget/models/budget.py b/budget/models/budget.py
new file mode 100644
index 00000000000..8874c7b77dc
--- /dev/null
+++ b/budget/models/budget.py
@@ -0,0 +1,158 @@
+from markupsafe import Markup
+from odoo import api, fields, models
+from odoo.exceptions import UserError
+from odoo.exceptions import ValidationError
+
+
+class BudgetBudget(models.Model):
+ _name = "budget.budget"
+ _description = "budget.budget"
+ _order = "duration_start_date"
+ _inherit = ["mail.thread", "mail.activity.mixin", "avatar.mixin"]
+
+ name = fields.Char()
+ description = fields.Char(compute="_compute_description")
+ active = fields.Boolean("active", default=True)
+ warning = fields.Boolean(compute="_compute_warraning_budget_line")
+
+ duration_start_date = fields.Date(
+ "duration_start_date",
+ )
+ duration_end_date = fields.Date(
+ "duration_end_date",
+ )
+ status = fields.Selection(
+ selection=[
+ ("Draft", "Draft"),
+ ("Confirmed", "Confirmed"),
+ ("Revised", "Revised"),
+ ("Done", "Done"),
+ ],
+ default="Draft",
+ )
+ over_budget = fields.Selection(
+ selection=[("warning", "warning"), ("restriction", "restriction")]
+ )
+ responsible = fields.Many2one(
+ "res.users", ondelete="restrict", default=lambda self: self.env.user
+ )
+ company_id = fields.Many2one(
+ "res.company", required=True, default=lambda self: self.env.company
+ )
+
+ revise_by = fields.Many2one("res.users", ondelete="restrict", copy=False)
+ budget_line_ids = fields.One2many(
+ "budget.line", inverse_name="budget_id", string="budget_lines", copy=True
+ )
+
+ message_ids = fields.One2many(
+ "mail.message",
+ "res_id",
+ domain=[("model", "=", "budget.budget"), ("model", "=", "budget.line")],
+ string="Messages",
+ )
+
+ @api.constrains("duration_start_date", "duration_end_date")
+ def _constrains_date(self):
+ for record in self:
+ if record.duration_start_date > record.duration_end_date:
+ raise UserError(
+ "Invalid duration! The start date cannot be later than the end date. Please select a valid date range."
+ )
+ budget = self.env["budget.budget"].search(
+ [
+ ("duration_start_date", "=", record.duration_start_date),
+ ("duration_end_date", "=", record.duration_end_date),
+ ("id", "!=", self.id),
+ ("active", "=", True),
+ ]
+ )
+ if budget:
+ raise ValidationError(
+ "Warning: A budget already exists for this duration."
+ )
+
+ @api.depends("name", "duration_start_date", "duration_end_date")
+ def _compute_description(self):
+ for record in self:
+ if record.name:
+ record.description = (
+ record.name
+ + ": "
+ + str(record.duration_start_date)
+ + " to "
+ + str(record.duration_end_date)
+ )
+ else:
+ record.description = ""
+
+ @api.depends("budget_line_ids.budget_achive")
+ def _compute_warraning_budget_line(self):
+ for record in self:
+ record.warning = True
+ if record.over_budget == "warning":
+ for line in record.budget_line_ids:
+ if line.budget_achive > line.budget_total:
+ record.warning = False
+
+ def action_draft(self):
+ for record in self:
+ record.status = "Draft"
+ return True
+
+ def action_confirm(self):
+ for record in self:
+ record.status = "Confirmed"
+ return True
+
+ def action_done(self):
+ for record in self:
+ record.status = "Done"
+ return True
+
+ def action_revise(self):
+ for record in self:
+ record.write(
+ {"revise_by": self.env.user, "status": "Revised", "active": False}
+ )
+ revised_budget = record.sudo().copy(
+ default={
+ "name": record.name,
+ "status": "Draft",
+ "duration_start_date": record.duration_start_date,
+ "duration_end_date": record.duration_end_date,
+ "active": True,
+ }
+ )
+ record.message_post(
+ body=Markup(
+ "%s: %s"
+ )
+ % (
+ "new budgetid",
+ revised_budget.id,
+ record.name,
+ )
+ )
+ return True
+
+ def action_budget_lines(self):
+ return {
+ "name": "Budget Lines",
+ "view_mode": "list,graph,pivot,gantt",
+ "res_model": "budget.line",
+ "type": "ir.actions.act_window",
+ "context": {
+ "default_budget_id": self.id,
+ },
+ "domain": [("budget_id", "=", self.id)],
+ }
+
+ def action_budget_form(self):
+ return {
+ "name": "Budget",
+ "view_mode": "form",
+ "res_model": "budget.budget",
+ "type": "ir.actions.act_window",
+ "res_id": self.id,
+ }
diff --git a/budget/models/budget_line.py b/budget/models/budget_line.py
new file mode 100644
index 00000000000..2816aa75aef
--- /dev/null
+++ b/budget/models/budget_line.py
@@ -0,0 +1,52 @@
+from odoo import api, fields, models
+from odoo.exceptions import ValidationError
+
+
+class BudgetLine(models.Model):
+ _name = "budget.line"
+ _description = "budget.line"
+
+ name = fields.Char(default="budget line")
+ budget_total = fields.Float()
+ budget_achive = fields.Float(compute="_compute_achieved_amount", store=True)
+ analytic_account_id = fields.Many2one(
+ "account.analytic.account",
+ )
+ budget_id = fields.Many2one("budget.budget", ondelete="cascade")
+ date_from = fields.Date(related="budget_id.duration_start_date", store=True)
+ date_to = fields.Date(related="budget_id.duration_end_date", store=True)
+ analytic_line_ids = fields.One2many("account.analytic.line", "budget_line_id")
+
+ @api.constrains("budget_achive")
+ def _constrains_amount(self):
+ for record in self:
+ if record.budget_id.over_budget == "restriction":
+ if record.budget_achive > record.budget_total:
+ raise ValidationError(
+ "Invalid amount! The achieved budget cannot exceed the total budget. Please adjust the amount accordingly."
+ )
+
+ @api.depends("analytic_line_ids.amount")
+ def _compute_achieved_amount(self):
+ for line in self:
+ line.budget_achive = abs(
+ sum(
+ line.analytic_line_ids.filtered(lambda l: l.amount < 0).mapped(
+ "amount"
+ )
+ )
+ )
+
+ def action_open_account_analytic(self):
+ return {
+ "name": "Analytic Lines",
+ "view_mode": "list,form",
+ "res_model": "account.analytic.line",
+ "type": "ir.actions.act_window",
+ "context": {
+ "default_budget_line_id": self.id,
+ "default_account_id": self.analytic_account_id.id,
+ "default_date": self.budget_id.duration_start_date,
+ },
+ "domain": [("budget_line_id", "=", self.id)],
+ }
diff --git a/budget/security/ir.model.access.csv b/budget/security/ir.model.access.csv
new file mode 100644
index 00000000000..15b071ed79e
--- /dev/null
+++ b/budget/security/ir.model.access.csv
@@ -0,0 +1,4 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+budget.access_budget_budget,access_budget_budget,budget.model_budget_budget,base.group_user,1,1,1,1
+budget.access_budget_line,access_budget_line,budget.model_budget_line,base.group_user,1,1,1,1
+budget.access_budget_wizard,access_budget_wizard,budget.model_budget_wizard,base.group_user,1,1,1,1
diff --git a/budget/static/budget.png b/budget/static/budget.png
new file mode 100644
index 00000000000..c14e39044dd
Binary files /dev/null and b/budget/static/budget.png differ
diff --git a/budget/views/account_analytic_line.xml b/budget/views/account_analytic_line.xml
new file mode 100644
index 00000000000..953aa9b521f
--- /dev/null
+++ b/budget/views/account_analytic_line.xml
@@ -0,0 +1,19 @@
+
+
+
+
+ account.analytic.line.form.inherit.account
+ account.analytic.line
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/budget/views/budget_budget_views.xml b/budget/views/budget_budget_views.xml
new file mode 100644
index 00000000000..45e2dba6892
--- /dev/null
+++ b/budget/views/budget_budget_views.xml
@@ -0,0 +1,129 @@
+
+
+
+
+ budget
+ budget.budget
+ kanban,form
+
+
+ Define new budgets
+
+
+
+
+
+ budget.budget.view.kanban
+ budget.budget
+
+
+
+
+
+
+
+
+ Configuration
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ budget.budget.form.view
+ budget.budget
+
+
+
+
+
diff --git a/budget/views/budget_line_views.xml b/budget/views/budget_line_views.xml
new file mode 100644
index 00000000000..50db088c2f8
--- /dev/null
+++ b/budget/views/budget_line_views.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+ budget.line
+ budget.line
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ budget.line
+ budget.line
+
+
+
+
+
+
+
+
+
+
+
+ budget.line
+ budget.line
+
+
+
+
+
+
+
+
+
+
+ budget.line
+ budget.line
+
+
+
+
+
+
+
+
diff --git a/budget/views/budget_menu.xml b/budget/views/budget_menu.xml
new file mode 100644
index 00000000000..1b359ac86ef
--- /dev/null
+++ b/budget/views/budget_menu.xml
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/budget/wizard/__init__.py b/budget/wizard/__init__.py
new file mode 100644
index 00000000000..b70dd08917a
--- /dev/null
+++ b/budget/wizard/__init__.py
@@ -0,0 +1 @@
+from . import budget_wizard
\ No newline at end of file
diff --git a/budget/wizard/budget_wizard.py b/budget/wizard/budget_wizard.py
new file mode 100644
index 00000000000..117a08999b0
--- /dev/null
+++ b/budget/wizard/budget_wizard.py
@@ -0,0 +1,63 @@
+from dateutil.relativedelta import relativedelta
+from datetime import date
+from odoo import fields, models
+
+
+class BudgetWizard(models.TransientModel):
+ _name = "budget.wizard"
+ _description = "budget.wizard"
+
+ date_start = fields.Date()
+ date_end = fields.Date()
+ duration = fields.Selection(
+ [("Monthly", "Monthly"), ("Quarterly", "Quarterly")], default="Monthly"
+ )
+ account_id = fields.Many2many(
+ "account.analytic.account",
+ )
+
+ def action_create_budgets(self):
+ if self.date_start > self.date_end:
+ raise ValueError("Start date must be before end date.")
+ step = relativedelta(months=1)
+ date_start = date(self.date_start.year, self.date_start.month, 1)
+ if self.duration == "Quarterly":
+ step = relativedelta(months=3)
+ if self.date_start.month % 3 == 2:
+ date_start = date(
+ self.date_start.year,
+ self.date_start.month + 1,
+ 1,
+ )
+ elif self.date_start.month % 3 == 0:
+ date_start = date(
+ self.date_start.year,
+ self.date_start.month + 2,
+ 1,
+ )
+
+ c = 1
+ while date_start <= self.date_end:
+ date_to = date_start + step - relativedelta(days=1)
+ self.env["budget.budget"].create(
+ {
+ "name": ("Budget " + str(c)),
+ "duration_start_date": date_start,
+ "duration_end_date": date_to,
+ "budget_line_ids": [
+ (
+ 0,
+ 0,
+ {
+ "name": (
+ "Budget " + str(c) + " " + analytic_account.name
+ ),
+ "analytic_account_id": analytic_account.id,
+ },
+ )
+ for analytic_account in self.account_id
+ ],
+ }
+ )
+ c += 1
+ date_start = date_to + relativedelta(days=1)
diff --git a/budget/wizard/budget_wizard_view.xml b/budget/wizard/budget_wizard_view.xml
new file mode 100644
index 00000000000..bb7f0c8e087
--- /dev/null
+++ b/budget/wizard/budget_wizard_view.xml
@@ -0,0 +1,36 @@
+
+
+
+
+ budget
+ budget.wizard
+ form
+ new
+
+
+
+ budget.budget.view.wizard
+ budget.wizard
+
+
+
+
+
+
diff --git a/estate/__init__.py b/estate/__init__.py
new file mode 100644
index 00000000000..429fa2b64e2
--- /dev/null
+++ b/estate/__init__.py
@@ -0,0 +1,3 @@
+from . import wizard
+from . import models
+from . import controllers
diff --git a/estate/__manifest__.py b/estate/__manifest__.py
new file mode 100644
index 00000000000..d665c7b2602
--- /dev/null
+++ b/estate/__manifest__.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+{
+ "name": "estate",
+ "author": "Odoo",
+ "website": "https://www.odoo.com",
+ "category": "Real Estate/Brokerage",
+ "version": "0.1",
+ # any module necessary for this one to work correctly
+ "depends": ["base", "website"],
+ "application": True,
+ "installable": True,
+ "data": [
+ "security/security.xml",
+ "security/ir.model.access.csv",
+ "views/property_website_list_template.xml",
+ "wizard/wizard_property_offers_view.xml",
+ "report/estate_property_templates.xml",
+ "report/estate_property_reports.xml",
+ "views/estate_property_offer_view.xml",
+ "views/estate_property_view.xml",
+ "views/estate_property_type_view.xml",
+ "views/inherited_user_view.xml",
+ "views/estate_property_menu.xml",
+ "data/estate.property.type.csv",
+ ],
+ "demo": ["demo/estate_property_demo.xml", "demo/estate_property_offer_demo.xml"],
+ "license": "AGPL-3",
+}
diff --git a/estate/controllers/__init__.py b/estate/controllers/__init__.py
new file mode 100644
index 00000000000..bc4ddcdbe07
--- /dev/null
+++ b/estate/controllers/__init__.py
@@ -0,0 +1 @@
+from . import estate_property_website
diff --git a/estate/controllers/estate_property_website.py b/estate/controllers/estate_property_website.py
new file mode 100644
index 00000000000..db5ab9306de
--- /dev/null
+++ b/estate/controllers/estate_property_website.py
@@ -0,0 +1,56 @@
+from odoo import http
+from odoo.http import request
+
+
+class Academy(http.Controller):
+ @http.route(
+ ["/properties", "/properties/page/"],
+ type="http",
+ auth="public",
+ website=True,
+ )
+ def index(self, page=1, **kw):
+ step = 6
+ offset = (page - 1) * step
+ total_properties = (
+ request.env["estate.property"]
+ .sudo()
+ .search_count(
+ [
+ "&",
+ ("status", "in", ["new", "offer_received", "offer_accepted"]),
+ ("active", "=", True),
+ ],
+ )
+ )
+ properties = (
+ request.env["estate.property"]
+ .sudo()
+ .search(
+ [
+ "&",
+ ("status", "in", ["new", "offer_received", "offer_accepted"]),
+ ("active", "=", True),
+ ],
+ limit=step,
+ offset=offset,
+ )
+ )
+
+ pager = request.website.pager(
+ url="/properties", total=total_properties, step=step, page=page
+ )
+
+ return request.render(
+ "estate.website_list_template",
+ {"properties": properties, "pager": pager},
+ )
+
+ @http.route(
+ "/property/",
+ type="http",
+ auth="public",
+ website=True,
+ )
+ def property_detail(self, property, **kwargs):
+ return request.render("estate.property_detail_page", {"property": property})
diff --git a/estate/data/estate.property.type.csv b/estate/data/estate.property.type.csv
new file mode 100644
index 00000000000..4752fac676e
--- /dev/null
+++ b/estate/data/estate.property.type.csv
@@ -0,0 +1,5 @@
+id,name
+property_type_1,Residential
+property_type_2,Commercial
+property_type_3,Industrial
+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..6c4c2a9c26b
--- /dev/null
+++ b/estate/demo/estate_property_demo.xml
@@ -0,0 +1,72 @@
+
+
+
+
+ Big Villa
+ new
+ A nice and big villa
+ 12345
+ 2020-02-02
+ 1600000
+
+ 6
+ 100
+ 4
+ True
+ True
+ 100000
+ south
+
+
+
+
+ Trailer home
+ cancelled
+ Home in a trailer park
+ 54321
+ 1970-01-01
+ 100000
+ 120000
+ 1
+ 10
+ 4
+ False
+
+
+
+
+
+
+ Beach home
+ new
+ Home in a Beach side in South Goa
+ 54321
+ 1970-01-01
+ 10000000
+
+ 6
+ 100
+ 4
+ False
+
+
+
+
+
+
+
+
diff --git a/estate/demo/estate_property_offer_demo.xml b/estate/demo/estate_property_offer_demo.xml
new file mode 100644
index 00000000000..11f31997f14
--- /dev/null
+++ b/estate/demo/estate_property_offer_demo.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+ 10000
+ 14
+
+
+
+
+
+
+ 1500000
+ 14
+
+
+
+
+
+
+ 1500001
+ 14
+
+
+
+
+
+
+
+
+ 1500000
+ 14
+
+
+
+
+
+
+ 1500001
+ 14
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/estate/models/__init__.py b/estate/models/__init__.py
new file mode 100644
index 00000000000..a58455cae67
--- /dev/null
+++ b/estate/models/__init__.py
@@ -0,0 +1,5 @@
+from . import estate_property
+from . import estate_property_type
+from . import estate_property_tags
+from . import estate_property_offer
+from . import inherited_user
diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py
new file mode 100644
index 00000000000..a69483e8757
--- /dev/null
+++ b/estate/models/estate_property.py
@@ -0,0 +1,170 @@
+from odoo import fields, models, api, _
+from datetime import datetime
+from odoo.exceptions import UserError
+from odoo.exceptions import ValidationError
+from dateutil.relativedelta import relativedelta
+
+
+class TestProperty(models.Model):
+ _name = "estate.property"
+ _description = "Test proerty"
+ _order = "id desc"
+
+ name = fields.Char("name", required=True, default="My new House")
+ description = fields.Text("description")
+ postcode = fields.Char()
+ date_availability = fields.Date(
+ "availability",
+ default=(datetime.today() + relativedelta(months=3)).date(),
+ copy=False,
+ )
+ expected_price = fields.Float()
+ best_price = fields.Float(readonly=True, compute="_compute_best_price")
+ selling_price = fields.Float(readonly=True, copy=False)
+ bedrooms = fields.Integer("bedrooms", default=2)
+ active = fields.Boolean("Active", default=True)
+
+ garden_area = fields.Integer()
+ living_area = fields.Integer()
+ total_area = fields.Integer(compute="_compute_total_area")
+
+ facades = fields.Integer()
+ garage = fields.Boolean()
+ garden = fields.Boolean()
+ garden_orientation = fields.Selection(
+ selection=[
+ ("north", "NORTH"),
+ ("south", "SOUTH"),
+ ("east", "EAST"),
+ ("west", "WEST"),
+ ]
+ )
+ status = fields.Selection(
+ [
+ ("new", "New"),
+ ("offer_received", "Offer Received"),
+ ("offer_accepted", "Offer Accepted"),
+ ("sold", "Sold"),
+ ("cancelled", "Cancelled"),
+ ],
+ default="new",
+ string="state",
+ required=True,
+ )
+ property_types_id = fields.Many2one("estate.property.type", ondelete="restrict")
+ property_tags_id = fields.Many2many("estate.property.tags")
+
+ buyer_id = fields.Many2one("res.partner", string="Buyer")
+ sales_person_id = fields.Many2one(
+ "res.users", ondelete="restrict", default=lambda self: self.env.user
+ )
+
+ property_offers_id = fields.One2many(
+ "estate.property.offer",
+ "property_id",
+ string="offer",
+ )
+ company_id = fields.Many2one(
+ "res.company", required=True, default=lambda self: self.env.company
+ )
+ image = fields.Image()
+
+ @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("property_offers_id.price")
+ def _compute_best_price(self):
+ for record in self:
+ if record.property_offers_id:
+ record.best_price = max(record.property_offers_id.mapped("price"))
+ else:
+ record.best_price = 0
+
+ @api.depends("garden_area", "living_area")
+ def _compute_total_area(self):
+ for record in self:
+ record.total_area = record.garden_area + record.living_area
+
+ @api.onchange("garden")
+ def _onchange_garden(self):
+ if self.garden:
+ self.garden_area = 10
+ self.garden_orientation = "north"
+ return {
+ "warning": {
+ "title": ("Warning"),
+ "message": ("you have checked garden button"),
+ }
+ }
+ else:
+ self.garden_area = 0
+
+ def action_sold(self):
+ for record in self:
+ if record.status == "sold":
+ raise UserError("property already soldout")
+ elif record.status == "offer_accepted":
+ record.status = "sold"
+ elif record.status != "cancelled":
+ raise UserError("Accepted any offer")
+ else:
+ raise UserError("Canceled property can't be sold")
+
+ return True
+
+ def property_cancle_action(self):
+ for record in self:
+ if record.status == "sold":
+ raise UserError("property already soldout")
+ else:
+ record.active = False
+ record.status = "cancelled"
+ return True
+
+ _sql_constraints = [
+ (
+ "check_expected_price",
+ "CHECK(expected_price > 0)",
+ "Expected price must be strictly positive",
+ ),
+ (
+ "check_selling_price",
+ "CHECK(selling_price >= 0)",
+ "Selling price must be non-negative",
+ ),
+ ]
+
+ # @api.constrains('property_offers_id')
+ # def _check_offer(self):
+ # for record in self:
+ # if record.property_offers_id:
+ # if record.status == "new":
+ # record.status = "offer_received"
+ # else:
+ # record.status = "new"
+
+ @api.ondelete(at_uninstall=False)
+ def _unlink_if_user_inactive(self):
+ for record in self:
+ if (
+ record.status == "sold"
+ or record.status == "offer_accepted"
+ or record.status == "offer_received"
+ ):
+ raise UserError(_("Can't delete this property! "))
+
+ def action_make_offer(self):
+ print(" makeoffers ".center(100, "="))
+ print(self)
+ return {
+ "type": "ir.actions.act_window",
+ "target": "new",
+ "res_model": "propery.offers",
+ "view_mode": "form",
+ # "context": {
+ # "default_property_ids": self.ids,
+ # },
+ }
+ # return self.env.ref("estate.property_offers_view_form")
diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py
new file mode 100644
index 00000000000..37c77f89ad1
--- /dev/null
+++ b/estate/models/estate_property_offer.py
@@ -0,0 +1,102 @@
+from datetime import datetime
+from dateutil.relativedelta import relativedelta
+from odoo import models, fields, api
+from odoo.exceptions import UserError
+from odoo.exceptions import ValidationError
+
+
+class estate_property_offer(models.Model):
+ _name = "estate.property.offer"
+ _description = "Test proerty Offer"
+
+ _order = "price desc"
+
+ price = fields.Float("price", required=True)
+ status = fields.Selection(
+ [("Accepted", "Accepted"), ("Refused", "Refused"), ("Pending", "Pending")],
+ default="Pending",
+ copy=False,
+ )
+ buyer_id = fields.Many2one("res.partner", required=True)
+ property_id = fields.Many2one("estate.property", required=True, ondelete="cascade")
+ #! if there is compute and inverse in model then both fields data have to give in demo data
+ # ! can't call compute or inverse if there is any default given in any feld
+ validity = fields.Integer(default=7)
+ date_deadline = fields.Date(
+ compute="_compute_date_deadline",
+ inverse="_inverse_date_deadline",
+ default=datetime.today(),
+ )
+ property_type_id = fields.Many2one(
+ related="property_id.property_types_id", string="property_type"
+ )
+
+ @api.depends("validity")
+ def _compute_date_deadline(self):
+ for record in self:
+ record.date_deadline = (
+ datetime.today() + relativedelta(days=record.validity)
+ ).date()
+
+ def _inverse_date_deadline(self):
+ for record in self:
+ if record.date_deadline:
+ record.validity = (record.date_deadline - datetime.today().date()).days
+ else:
+ record.date_deadline = 0
+
+ def property_accepted(self):
+ for record in self:
+ if record.property_id.status != "offer_accepted":
+ if record.status == "Pending":
+ record.status = "Accepted"
+ record.property_id.selling_price = record.price
+ record.property_id.buyer_id = record.buyer_id
+ record.property_id.status = "offer_accepted"
+
+ else:
+ raise UserError(" offer already accepted")
+
+ def property_rejected(self):
+ for record in self:
+ if record.property_id.status != "cancelled":
+ if record.status == "Accepted" or record.status == "Pending":
+ if record.status == "Accepted":
+ record.property_id.selling_price = 0
+ record.status = "Refused"
+ record.property_id.status = "offer_received"
+
+ else:
+ record.status = "Pending"
+
+ @api.constrains("price", "status")
+ def _check_offer_constraint(self):
+ for record in self:
+ if (
+ record.price / record.property_id.expected_price * 100
+ ) < 90 and record.status == "Accepted":
+ raise ValidationError(
+ "The selling price must be atleast 90 percentage of expected price"
+ )
+
+ return True
+
+ @api.model_create_multi
+ def create(self, vals_list):
+ for vals in vals_list:
+ if vals["property_id"]:
+ prop = self.env["estate.property"].browse(vals["property_id"])
+ if vals["price"] < prop.best_price:
+ raise UserError("price must be greater than best price")
+ if prop.status == "new":
+ prop.status = "offer_received"
+ return super().create(vals_list)
+
+ # @api.model
+ # def create(self , vals_list):
+ # prop = self.env["estate.property"].browse(vals_list["property_id"])
+ # if vals_list["price"] < prop.best_price:
+ # raise UserError('price must be greater than best price')
+ # if prop.status == "new":
+ # prop.status = "offer_received"
+ # return super().create(vals_list)
diff --git a/estate/models/estate_property_tags.py b/estate/models/estate_property_tags.py
new file mode 100644
index 00000000000..820801d988a
--- /dev/null
+++ b/estate/models/estate_property_tags.py
@@ -0,0 +1,14 @@
+from odoo import models, fields
+from random import randint
+
+
+class TestPropertyTags(models.Model):
+ _name = "estate.property.tags"
+ _description = "Test proerty Tags"
+ _order = "name"
+
+ name = fields.Char("name")
+ color = fields.Integer("Color", default=lambda self: self._default_color())
+
+ def _default_color(self):
+ return randint(1, 11)
diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py
new file mode 100644
index 00000000000..b3d39b80ca3
--- /dev/null
+++ b/estate/models/estate_property_type.py
@@ -0,0 +1,25 @@
+from odoo import models, fields, api
+
+
+class TestPropertyType(models.Model):
+ _name = "estate.property.type"
+ _description = "Test proerty Type"
+
+ _order = "name"
+
+ name = fields.Char("name")
+ propertys_id = fields.One2many("estate.property", "property_types_id")
+ sequence = fields.Integer("Sequence", default=1, help="Used to order type")
+ offer_ids = fields.One2many(
+ "estate.property.offer", "property_type_id", string="offers"
+ )
+ offer_count = fields.Integer(readonly=True, compute="_compute_count_offer")
+
+ @api.depends("offer_ids")
+ def _compute_count_offer(self):
+ for record in self:
+ record.offer_count = len(self.offer_ids)
+
+ _sql_constraints = [
+ ("name_uniq", "unique(name)", "Type must be unique"),
+ ]
diff --git a/estate/models/inherited_user.py b/estate/models/inherited_user.py
new file mode 100644
index 00000000000..9719bdcc575
--- /dev/null
+++ b/estate/models/inherited_user.py
@@ -0,0 +1,10 @@
+from odoo import fields, models
+from dateutil.relativedelta import relativedelta
+
+
+class Inherited_User(models.Model):
+ _inherit = "res.users"
+
+ property_ids = fields.One2many(
+ "estate.property", "sales_person_id", string="property"
+ )
diff --git a/estate/report/estate_property_reports.xml b/estate/report/estate_property_reports.xml
new file mode 100644
index 00000000000..13e0ae2e2ad
--- /dev/null
+++ b/estate/report/estate_property_reports.xml
@@ -0,0 +1,27 @@
+
+
+
+
+ Property Offers
+ estate.property
+ qweb-pdf
+ estate.estate_property_offer_template
+ estate.estate_property_offer_template
+ ' %s property offers' % (object.name)
+
+ report
+
+
+
+
+ users Offers
+ res.users
+ qweb-pdf
+ estate.test_user_offer_template
+ estate.test_user_offer_template
+ ' %s property offers' % (object.name)
+
+ report
+
+
+
diff --git a/estate/report/estate_property_templates.xml b/estate/report/estate_property_templates.xml
new file mode 100644
index 00000000000..da17b4ab31e
--- /dev/null
+++ b/estate/report/estate_property_templates.xml
@@ -0,0 +1,127 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Expected Price:
+
+
+
+ Status:
+
+
+
+ !!!Invoice has already been created !!!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Salesman:
+
+
+
+ Expected Price:
+
+
+
+ Status:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ buyer_name |
+ validity |
+ date_deadline |
+ Price |
+ status |
+
+
+
+
+
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+ there is no offer in this property
+
+
+
+
+
+
+
+ !!!Invoice has already been created !!!
+
+
+
+
+
+
diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv
new file mode 100644
index 00000000000..c7e1721b4ed
--- /dev/null
+++ b/estate/security/ir.model.access.csv
@@ -0,0 +1,10 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+estate.access_estate_property_manager,access_estate_property_manager,model_estate_property,estate.estate_group_manager,1,1,1,0
+estate.access_estate_property_type_manager,access_estate_property_type_manager,model_estate_property_type,estate.estate_group_manager,1,1,1,1
+estate.access_estate_property_tags_manager,access_estate_property_tags_manager,model_estate_property_tags,estate.estate_group_manager,1,1,1,1
+estate.access_estate_property_offer_manager,access_estate_property_offe_managerr,model_estate_property_offer,estate.estate_group_manager,1,1,1,1
+estate.access_estate_property_user,access_estate_property_user,model_estate_property,estate.estate_group_user,1,1,1,0
+estate.access_estate_property_type_user,access_estate_property_type_user,model_estate_property_type,estate.estate_group_user,1,0,0,0
+estate.access_estate_property_tags_user,access_estate_property_tags_user,model_estate_property_tags,estate.estate_group_user,1,0,0,0
+estate.access_estate_property_offer_user,access_estate_property_offer_user,model_estate_property_offer,estate.estate_group_user,1,1,1,0
+estate.access_propery_offers,access_propery_offers,estate.model_propery_offers,base.group_user,1,1,1,0
diff --git a/estate/security/security.xml b/estate/security/security.xml
new file mode 100644
index 00000000000..1dc64382a95
--- /dev/null
+++ b/estate/security/security.xml
@@ -0,0 +1,39 @@
+
+
+
+
+ Agent
+
+ this group is for real estate broker
+
+
+
+ Manager
+
+ this group is for real estate Manager
+
+
+
+
+
+
+
+
+ Agent view
+
+
+
+ [ ('company_id', 'in', company_ids)
+ ]
+
+
+
+ manager view
+
+
+ []
+
+
+
+
+
diff --git a/estate/views/estate_property_menu.xml b/estate/views/estate_property_menu.xml
new file mode 100644
index 00000000000..35caacecde5
--- /dev/null
+++ b/estate/views/estate_property_menu.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
diff --git a/estate/views/estate_property_offer_view.xml b/estate/views/estate_property_offer_view.xml
new file mode 100644
index 00000000000..fa64d57c17b
--- /dev/null
+++ b/estate/views/estate_property_offer_view.xml
@@ -0,0 +1,34 @@
+
+
+
+ estate.property.offer
+ estate.property.offer
+ list,form
+ [('property_type_id', '=', active_id)]
+
+
+
+
+
+
+ estate.property.offer
+ estate.property.offer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/estate/views/estate_property_type_view.xml b/estate/views/estate_property_type_view.xml
new file mode 100644
index 00000000000..b43bc0d52e7
--- /dev/null
+++ b/estate/views/estate_property_type_view.xml
@@ -0,0 +1,68 @@
+
+
+
+
+ property types
+ estate.property.type
+ list,form
+
+
+
+
+
+ estate.property.type
+ estate.property.type
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ estate.property.type
+ estate.property.type
+
+
+
+
+
+
+
+
+
+
+
diff --git a/estate/views/estate_property_view.xml b/estate/views/estate_property_view.xml
new file mode 100644
index 00000000000..c79cb7d1f0d
--- /dev/null
+++ b/estate/views/estate_property_view.xml
@@ -0,0 +1,233 @@
+
+
+
+
+ property
+ estate.property
+ list,kanban,form
+
+
+ Define new property
+
+
+ {'search_default_available':True}
+
+
+
+
+
+ estate.property
+ estate.property
+
+
+
+
+
+
+
+
+
+
+
+
+ This is new!
+
+
+
+ expected price
+
+
+
+ selling price
+
+
+
+ best price
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ estate.property
+ estate.property
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ estate.property
+ estate.property
+
+
+
+
+
+
+
+
+ estate.property
+ estate.property
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Make Offer
+
+
+ code
+ action=records.action_make_offer()
+ list
+
+
+
+
+
diff --git a/estate/views/inherited_user_view.xml b/estate/views/inherited_user_view.xml
new file mode 100644
index 00000000000..28ce4ad7ea4
--- /dev/null
+++ b/estate/views/inherited_user_view.xml
@@ -0,0 +1,17 @@
+
+
+
+
+ res.user.inherit.user
+ res.users
+
+
+
+
+
+
+
+
+
+
+
diff --git a/estate/views/property_website_list_template.xml b/estate/views/property_website_list_template.xml
new file mode 100644
index 00000000000..2c53ddc94fc
--- /dev/null
+++ b/estate/views/property_website_list_template.xml
@@ -0,0 +1,173 @@
+
+
+
+
+
Estate Properties
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+ Postcode:
+
+
+
+
+ -
+
+ Date Availability:
+
+
+
+
+
+
+
+
+
+ -
+
+ Expected Price:
+
+ $
+
+
+
+
+
+
+
+
Room Features
+
+
+
+ -
+
+ Bedrooms:
+
+
+
+
+ -
+
+ Living Area:
+
+ m²
+
+
+ -
+
+ Garage:
+
+
+
+
+
+
+
+
+
+ -
+
+ Garden:
+
+
+
+
+
+
+
+ -
+
+ Garden Area:
+
+ m²
+
+
+ -
+
+ Garden Orientation:
+
+
+
+
+
+ -
+
+ Total Area:
+
+
+ m²
+
+
+
+
+
+
+
+
+
diff --git a/estate/wizard/__init__.py b/estate/wizard/__init__.py
new file mode 100644
index 00000000000..c006cc17e15
--- /dev/null
+++ b/estate/wizard/__init__.py
@@ -0,0 +1 @@
+from . import property_offers
diff --git a/estate/wizard/property_offers.py b/estate/wizard/property_offers.py
new file mode 100644
index 00000000000..c49c0984750
--- /dev/null
+++ b/estate/wizard/property_offers.py
@@ -0,0 +1,39 @@
+from odoo import api, fields, models
+
+
+class ProperyOffers(models.TransientModel):
+ _name = "propery.offers"
+ _description = "TransientModelmodel for offers"
+
+ price = fields.Float("price", required=True)
+ # property_ids = fields.Many2many("estate.property", string="property_id")
+ buyer_id = fields.Many2one("res.partner", required=True)
+ validity = fields.Integer(default=7)
+
+ def make_offer(self):
+ print(self)
+ failed_properties = []
+ for property in self.env.context.get("active_ids"):
+ try:
+ self.env["estate.property.offer"].create(
+ {
+ "price": self.price,
+ "property_id": property,
+ "buyer_id": self.buyer_id.id,
+ "validity": self.validity,
+ }
+ )
+ except Exception as e:
+ print(f"{e} exception in ", property)
+ failed_properties.append(str(e) + " in " + str(property))
+ if failed_properties:
+ return {
+ "type": "ir.actions.client",
+ "tag": "display_notification",
+ "params": {
+ "message": " ".join(failed_properties),
+ "type": "danger",
+ "next": {"type": "ir.actions.act_window_close"},
+ },
+ }
+ return {"type": "ir.actions.act_window_close"}
diff --git a/estate/wizard/wizard_property_offers_view.xml b/estate/wizard/wizard_property_offers_view.xml
new file mode 100644
index 00000000000..fd87021dbee
--- /dev/null
+++ b/estate/wizard/wizard_property_offers_view.xml
@@ -0,0 +1,31 @@
+
+
+
+ property offers
+ propery.offers
+ form
+
+
+
+
+ propery.offers
+ propery.offers
+
+
+
+
+
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..60faa0228e8
--- /dev/null
+++ b/estate_account/__manifest__.py
@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+{
+ "name": "estate_account",
+ "author": "Odoo",
+ "website": "https://www.odoo.com",
+ "category": "Tutorials/estate_account",
+ "version": "0.1",
+ # any module necessary for this one to work correctly
+ "depends": ["base", "web", "estate", "account"],
+ "application": True,
+ "installable": True,
+ "data": [],
+ "license": "AGPL-3",
+}
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..b4d5105e6d0
--- /dev/null
+++ b/estate_account/models/estate_property.py
@@ -0,0 +1,64 @@
+from odoo import models, Command
+from datetime import datetime
+
+
+class Property_Invoice(models.Model):
+ _inherit = "estate.property"
+
+ def action_sold(self):
+ print(" invoice ".center(100, "="))
+ self.check_access("write")
+ self.env["account.move"].sudo().create(
+ {
+ "partner_id": self.buyer_id.id,
+ "move_type": "out_invoice",
+ "invoice_line_ids": [
+ Command.create(
+ {
+ "name": self.name,
+ "quantity": 1,
+ "price_unit": self.selling_price,
+ }
+ ),
+ Command.create(
+ {
+ "name": "tax 6%",
+ "quantity": 1,
+ "price_unit": self.selling_price * 0.06,
+ }
+ ),
+ Command.create(
+ {
+ "name": "administrative fees",
+ "quantity": 1,
+ "price_unit": 100.00,
+ }
+ ),
+ ],
+ }
+ ).action_post()
+ # self.env["account.move"].sudo().new(
+ # {
+ # "partner_id": self.buyer_id.id,
+ # "move_type": "out_invoice",
+ # "invoice_line_ids": [
+ # {
+ # "name": self.name,
+ # "quantity": 1,
+ # "price_unit": self.selling_price,
+ # },
+ # {
+ # "name": "tax 6%",
+ # "quantity": 1,
+ # "price_unit": self.selling_price * 0.06,
+ # },
+ # {
+ # "name": "administrative fees",
+ # "quantity": 1,
+ # "price_unit": 100.00,
+ # },
+ # ],
+ # }
+ # ).action_post()
+
+ return super().action_sold()
diff --git a/product_warranty/__init__.py b/product_warranty/__init__.py
new file mode 100644
index 00000000000..9a7e03eded3
--- /dev/null
+++ b/product_warranty/__init__.py
@@ -0,0 +1 @@
+from . import models
\ No newline at end of file
diff --git a/product_warranty/__manifest__.py b/product_warranty/__manifest__.py
new file mode 100644
index 00000000000..e94f4e96c1b
--- /dev/null
+++ b/product_warranty/__manifest__.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+{
+ "name": "product_warranty",
+ "summary": "Add product warranty",
+ "description": "Add product warranty (yame)",
+ "author": "Odoo",
+ "website": "https://www.odoo.com",
+ "category": "Tutorials/product_warranty",
+ "version": "0.1",
+ # any module necessary for this one to work correctly
+ "depends": ["base", "web", "stock", "sale_management"],
+ "application": True,
+ "installable": True,
+ "data": [
+ "security/ir.model.access.csv",
+ "views/product_views.xml",
+ ],
+ "license": "AGPL-3",
+}
diff --git a/product_warranty/models/__init__.py b/product_warranty/models/__init__.py
new file mode 100644
index 00000000000..ef24594fa25
--- /dev/null
+++ b/product_warranty/models/__init__.py
@@ -0,0 +1,2 @@
+from . import product_template
+from . import warranty
diff --git a/product_warranty/models/product_template.py b/product_warranty/models/product_template.py
new file mode 100644
index 00000000000..3daee771a98
--- /dev/null
+++ b/product_warranty/models/product_template.py
@@ -0,0 +1,10 @@
+from odoo import api, fields, models
+
+
+class ProductTemplate(models.Model):
+ _inherit = "product.template"
+
+ is_warranty = fields.Boolean(default=False, string="Warranty")
+ warranty_configuration_ids = fields.One2many(
+ "product.warranty.configuration", "product_template_id", string="Add Warranty"
+ )
diff --git a/product_warranty/models/warranty.py b/product_warranty/models/warranty.py
new file mode 100644
index 00000000000..de6c11feb03
--- /dev/null
+++ b/product_warranty/models/warranty.py
@@ -0,0 +1,22 @@
+from odoo import api, fields, models
+from odoo.exceptions import ValidationError
+
+
+class WarrantyConfiguration(models.Model):
+ _name = "product.warranty.configuration"
+ _description = "product.warranty.configuration"
+
+ name = fields.Char(string="name", required=True)
+ product_template_id = fields.Many2one(
+ "product.template", required=True, ondelete="cascade"
+ )
+ percentage = fields.Float()
+ years = fields.Integer(default=1)
+
+ @api.onchange("percentage")
+ def _onchange_percentage(self):
+ for record in self:
+ if record.percentage < 0 or record.percentage > 100:
+ raise ValidationError(
+ "The value of 'Your Float Field' must be between 0 and 100."
+ )
diff --git a/product_warranty/security/ir.model.access.csv b/product_warranty/security/ir.model.access.csv
new file mode 100644
index 00000000000..68c6466db32
--- /dev/null
+++ b/product_warranty/security/ir.model.access.csv
@@ -0,0 +1,3 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+product_warranty.access_product_warranty_configuration,access_product_warranty_configuration,product_warranty.model_product_warranty_configuration,base.group_user,1,0,0,0
+product_warranty.access_product_warranty_configuration,access_product_warranty_configuration,product_warranty.model_product_warranty_configuration,product.group_product_manager,1,1,1,1
diff --git a/product_warranty/views/product_views.xml b/product_warranty/views/product_views.xml
new file mode 100644
index 00000000000..b4de0819d6e
--- /dev/null
+++ b/product_warranty/views/product_views.xml
@@ -0,0 +1,26 @@
+
+
+
+
+ product.template.common.form
+ product.template
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/product_warranty/views/sale_menus.xml b/product_warranty/views/sale_menus.xml
new file mode 100644
index 00000000000..b910f1a9b44
--- /dev/null
+++ b/product_warranty/views/sale_menus.xml
@@ -0,0 +1,8 @@
+