diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py index 31406e8addb..ca5d5092218 100644 --- a/awesome_dashboard/__manifest__.py +++ b/awesome_dashboard/__manifest__.py @@ -23,7 +23,10 @@ ], 'assets': { 'web.assets_backend': [ - 'awesome_dashboard/static/src/**/*', + 'awesome_dashboard/static/src/dashboard_action.js', + ], + 'awesome_dashboard.dashboard': [ + 'awesome_dashboard/static/src/dashboard/**/*', ], }, 'license': 'AGPL-3' diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js deleted file mode 100644 index 637fa4bb972..00000000000 --- a/awesome_dashboard/static/src/dashboard.js +++ /dev/null @@ -1,10 +0,0 @@ -/** @odoo-module **/ - -import { Component } from "@odoo/owl"; -import { registry } from "@web/core/registry"; - -class AwesomeDashboard extends Component { - static template = "awesome_dashboard.AwesomeDashboard"; -} - -registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboard); diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard.xml deleted file mode 100644 index 1a2ac9a2fed..00000000000 --- a/awesome_dashboard/static/src/dashboard.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - hello dashboard - - - diff --git a/awesome_dashboard/static/src/dashboard/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js new file mode 100644 index 00000000000..caafe61dedb --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -0,0 +1,57 @@ +/** @odoo-module **/ + +import { Component, useState } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { Layout } from "@web/search/layout"; +import { useService } from "@web/core/utils/hooks"; +import { DashboardItem } from "./dashboard_item/dashboard_item"; +import { DashboardSettings } from "./dashboard_settings/dashboard_settings"; +import { browser } from "@web/core/browser/browser"; + +class AwesomeDashboard extends Component { + static template = "awesome_dashboard.AwesomeDashboard"; + static components = { Layout, DashboardItem }; + + setup() { + this.actionService = useService("action"); + this.statisticsService = useService("awesome_dashboard.statistics"); + this.statistics = useState(this.statisticsService.data); + + this.dialogService = useService("dialog"); + + const dashboardItemsRegistry = registry.category("awesome_dashboard"); + this.items = dashboardItemsRegistry.getAll(); + + this.state = useState({ + uncheckedItems: browser.localStorage.getItem("uncheckedItems")?.split(",").filter(id => id) || [], + }); + } + + updateConfiguration(newUncheckedItems) { + this.state.uncheckedItems.length = 0; + this.state.uncheckedItems.push(...newUncheckedItems); + } + + openConfiguration() { + this.dialogService.add(DashboardSettings, { + items: this.items, + initialUncheckedItems: this.state.uncheckedItems, + updateConfiguration: this.updateConfiguration.bind(this), + }); + } + + openCustomers() { + this.actionService.doAction("base.action_partner_form"); + } + + openLeads() { + this.actionService.doAction({ + type: "ir.actions.act_window", + name: "Leads", + res_model: "crm.lead", + views: [[false, "list"], [false, "form"]], + }); + } +} + +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..90e1493325f --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.scss @@ -0,0 +1,3 @@ +.o_dashboard { + background-color: grey; +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml new file mode 100644 index 00000000000..eae4f3faf9e --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + +
+
+ + + + + + +
+
+
+
+
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..6aeb93e2446 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js @@ -0,0 +1,19 @@ +/** @odoo-module **/ +import { Component } from "@odoo/owl"; + +export class DashboardItem extends Component { + static template = "awesome_dashboard.DashboardItem"; + + static props = { + size: { + type: Number, + optional: true, + default: 1, + }, + slots: { + type: Object, + optional: true, + shape: {default: Object} + } + } +} 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..213ca6e0b67 --- /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..067739d5770 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js @@ -0,0 +1,67 @@ +/** @odoo-module **/ + +import { NumberCard } from "./number_card/number_card"; +import { PieChartCard } from "./piechart_card/piechart_card"; +import { registry } from "@web/core/registry"; + +export const items = [ + { + id: "nb_new_orders", + description: "Number of new orders this month", + Component: NumberCard, + props: (data) => ({ + title: "Number of new orders this month", + value: data.nb_new_orders + }), + }, + { + id: "total_amount", + description: "Total amount of new orders this month", + Component: NumberCard, + props: (data) => ({ + title: "Total amount of new orders this month", + value: data.total_amount + }), + }, + { + id: "average_quantity", + description: "Average amount of t-shirt", + Component: NumberCard, + props: (data) => ({ + title: "Average amount of t-shirt by order this month", + value: data.average_quantity + }), + }, + { + id: "nb_cancelled_orders", + description: "Number of cancelled orders this month", + Component: NumberCard, + props: (data) => ({ + title: "Number of cancelled orders this month", + value: data.nb_cancelled_orders + }), + }, + { + id: "average_time", + description: "Average time for order processing", + Component: NumberCard, + props: (data) => ({ + title: "Average time for an order to go from new to sent or cancelled", + value: `${data.average_time} hours` + }), + }, + { + id: "orders_by_size_chart", + description: "T-Shirt Sales by Size Chart", + Component: PieChartCard, + size: 2, + props: (data) => ({ + title: "T-Shirt Sales by Size", + data: data + }), + }, +]; + +items.forEach((item) => { + registry.category("awesome_dashboard").add(item.id, item) +}); diff --git a/awesome_dashboard/static/src/dashboard/dashboard_settings/dashboard_settings.js b/awesome_dashboard/static/src/dashboard/dashboard_settings/dashboard_settings.js new file mode 100644 index 00000000000..45e85f2205e --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_settings/dashboard_settings.js @@ -0,0 +1,44 @@ +/** @odoo-module **/ + +import { Component, useState } from "@odoo/owl"; +import { Dialog } from "@web/core/dialog/dialog"; +import { browser } from "@web/core/browser/browser"; + +export class DashboardSettings extends Component { + static template = "awesome_dashboard.DashboardSettings"; + static components = { Dialog }; + + static props = { + close: { type: Function }, + }; + + setup() { + const items = this.props.items || {}; + const initialUncheckedItems = this.props.initialUncheckedItems || []; + + this.dialogDisplayItems = useState( + Object.values(items).map((item) => ({ + ...item, + checked: !initialUncheckedItems.includes(item.id), + })) + ); + } + + onChange(checked, itemInDialog) { + const targetItem = this.dialogDisplayItems.find(i => i.id === itemInDialog.id); + if (targetItem) { + targetItem.checked = checked; + } + } + + confirmChanges() { + const newUncheckedItems = this.dialogDisplayItems.filter((item) => !item.checked).map((item) => item.id); + + browser.localStorage.setItem("uncheckedItems", newUncheckedItems.join(",")); + + if (this.props.updateConfiguration) { + this.props.updateConfiguration(newUncheckedItems); + } + this.props.close(); + } +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard_settings/dashboard_settings.xml b/awesome_dashboard/static/src/dashboard/dashboard_settings/dashboard_settings.xml new file mode 100644 index 00000000000..a5b02c81169 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_settings/dashboard_settings.xml @@ -0,0 +1,26 @@ + + + + +
+

Select items to display on your dashboard:

+
+ + +
+
+ + + + +
+
+
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..60e661b99d3 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/number_card/number_card.js @@ -0,0 +1,11 @@ +/** @odoo-module **/ + +import { Component } from "@odoo/owl"; + +export class NumberCard extends Component { + static template = "awesome_dashboard.NumberCard"; + static props = { + title: { type: String }, + value: { type: [String, Number] }, + }; +} 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..f7d50ce9e90 --- /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..5580d146fb5 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.js @@ -0,0 +1,91 @@ +/** @odoo-module **/ +import { Component, onWillStart, onMounted, onWillUnmount, useRef, useEffect } from "@odoo/owl"; +import { loadJS } from "@web/core/assets"; + +export class PieChart extends Component { + static template = "awesome_dashboard.PieChart"; + static props = { + data: { type: Object }, + }; + + setup() { + this.chart = null; + this.canvasRef = useRef("canvas"); + + onWillStart(async () => { + await loadJS("/web/static/lib/Chart/Chart.js"); + }); + + onMounted(() => { + this.renderChart(); + }); + + // Re-render chart when data changes + useEffect(() => { + if (this.canvasRef.el) { + this.renderChart(); + } + }, () => [this.props.data.orders_by_size]); + + + onWillUnmount(() => { + if (this.chart) { + this.chart.destroy(); + } + }); + } + + renderChart() { + if (this.chart) { + this.chart.destroy(); + } + + const ordersBySizeData = this.props.data?.orders_by_size || {}; + const labels = Object.keys(ordersBySizeData); + const values = Object.values(ordersBySizeData); + + if (labels.length === 0) { + return; + } + + const colors = [ + '#FF6384','#36A2EB','#FFCE56','#4BC0C0','#9966FF','#FF9F40' + ]; + + this.chart = new Chart(this.canvasRef.el, { + type: 'pie', + data: { + labels: labels.map(label => label.toUpperCase()), + datasets: [{ + data: values, + backgroundColor: colors.slice(0, labels.length), + borderWidth: 2, + borderColor: '#fff' + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'bottom', + labels: { + padding: 15, + usePointStyle: true, + pointStyle: 'rect' + } + }, + title: { + display: true, + text: 'T-Shirt Sales by Size', + font: { + size: 16, + weight: 'bold' + }, + padding: 20 + } + } + } + }); + } +} 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..a04df27478b --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/pie_chart/pie_chart.xml @@ -0,0 +1,8 @@ + + + +
+ +
+
+
diff --git a/awesome_dashboard/static/src/dashboard/piechart_card/piechart_card.js b/awesome_dashboard/static/src/dashboard/piechart_card/piechart_card.js new file mode 100644 index 00000000000..033586f7d81 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/piechart_card/piechart_card.js @@ -0,0 +1,13 @@ +/** @odoo-module **/ + +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, optional: true }, + data: { type: Object }, + }; +} diff --git a/awesome_dashboard/static/src/dashboard/piechart_card/piechart_card.xml b/awesome_dashboard/static/src/dashboard/piechart_card/piechart_card.xml new file mode 100644 index 00000000000..2803b633f47 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/piechart_card/piechart_card.xml @@ -0,0 +1,9 @@ + + + +
+
+ +
+
+
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..72a9b8a9d80 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/statistics_service.js @@ -0,0 +1,32 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; +import { rpc } from "@web/core/network/rpc"; +import { memoize } from "@web/core/utils/functions"; +import { reactive } from "@odoo/owl"; + + +const statisticsService = { + start() { + const data = reactive({}); + const loadStatistics = async () => { + try { + const result = await rpc("/awesome_dashboard/statistics"); + Object.keys(data).forEach(key => delete data[key]); + Object.assign(data, result); + + } catch (error) { + console.error('Error loading statistics:', error); + } + }; + loadStatistics(); + + setInterval(loadStatistics, 50000); + + return { + data, + }; + }, +}; + +registry.category("services").add("awesome_dashboard.statistics", statisticsService); diff --git a/awesome_dashboard/static/src/dashboard_action.js b/awesome_dashboard/static/src/dashboard_action.js new file mode 100644 index 00000000000..763f7fc55da --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_action.js @@ -0,0 +1,14 @@ +/** @odoo-module **/ + +import { Component, xml } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { LazyComponent } from "@web/core/assets"; + +export class AwesomeDashboardLoader extends Component { + static components = { LazyComponent }; + static template = xml` + + `; +} + +registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboardLoader); diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js new file mode 100644 index 00000000000..f86fbab84c0 --- /dev/null +++ b/awesome_owl/static/src/card/card.js @@ -0,0 +1,21 @@ +/** @odoo-module **/ + +import { Component, useState } from "@odoo/owl"; + +export class Card extends Component { + static template = "awesome_owl.card"; + static props = { + title: { type: String, optional: true, default: "Card Title" }, + slots: {type: Object, optional: true}, + }; + + setup() { + // Add state to track if card is open (default: true) + this.state = useState({ isOpen: true }); + } + + toggleOpen() { + // Toggle the open/closed state + this.state.isOpen = !this.state.isOpen; + } +} diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml new file mode 100644 index 00000000000..48aca8440c3 --- /dev/null +++ b/awesome_owl/static/src/card/card.xml @@ -0,0 +1,20 @@ + + + +
+
+
+
+ +
+ +
+ +
+
+
+
+
diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js new file mode 100644 index 00000000000..aec4b1fda77 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.js @@ -0,0 +1,23 @@ +/** @odoo-module **/ + +import { Component, useState } from "@odoo/owl"; + +export class Counter extends Component { + static template = "awesome_owl.counter"; + + setup() { + this.state = useState({ value: 0 }); + } + + + static props = { + onChange: { type: Function, optional: true }, + }; + + increment() { + this.state.value += 1; + if (this.props.onChange) { + this.props.onChange(); + } + } +} diff --git a/awesome_owl/static/src/counter/counter.xml b/awesome_owl/static/src/counter/counter.xml new file mode 100644 index 00000000000..eab8b0e2c95 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.xml @@ -0,0 +1,7 @@ + + + +

Counter :

+ +
+
diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 657fb8b07bb..6a8d5564e14 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,7 +1,19 @@ /** @odoo-module **/ -import { Component } from "@odoo/owl"; +import { Component, useState } from "@odoo/owl"; +import { Counter } from "./counter/counter"; +import { Card } from "./card/card"; +import { TodoList } from "./todo_list/todo_list"; export class Playground extends Component { static template = "awesome_owl.playground"; + static components = { Counter, Card, TodoList}; + + setup() { + this.state = useState({sum: 0}) + } + + incrementSum() { + this.state.sum += 1; + } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..d326d381eb1 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -2,9 +2,36 @@ -
- hello world +
+

Owl Playground

+
+
+
+
Counters
+

Sum:

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

This is another card. Add your content here!

+
+
+
+
+
+
+
Todo List
+ +
+
+
- diff --git a/awesome_owl/static/src/todo_list/todo_items.js b/awesome_owl/static/src/todo_list/todo_items.js new file mode 100644 index 00000000000..85630f8c450 --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_items.js @@ -0,0 +1,30 @@ +/** @odoo-module **/ + +import { Component } from "@odoo/owl"; + +export class TodoItem extends Component { + static template = "awesome_owl.todo_item"; + static props = { + todo: { + type: Object, + shape: { + id: { type: Number, optional: false }, + description: { type: String, optional: false }, + isCompleted: { type: Boolean, optional: false }, + }, + optional: false, + }, + toggleState: { type: Function, optional: false }, + removeTodo: { type: Function, optional: false }, // New callback prop for deletion + }; + + toggleState() { + // Call the parent's toggleState function with the todo id + this.props.toggleState(this.props.todo.id); + } + + removeTodo() { + // Call the parent's removeTodo function with the todo id + this.props.removeTodo(this.props.todo.id); + } +} diff --git a/awesome_owl/static/src/todo_list/todo_items.xml b/awesome_owl/static/src/todo_list/todo_items.xml new file mode 100644 index 00000000000..bfc953e9c3d --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_items.xml @@ -0,0 +1,13 @@ + + + +
+ + . + +
+
+
diff --git a/awesome_owl/static/src/todo_list/todo_list.js b/awesome_owl/static/src/todo_list/todo_list.js new file mode 100644 index 00000000000..ccefb543ba1 --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_list.js @@ -0,0 +1,43 @@ +import { Component, useState } from "@odoo/owl"; +import { TodoItem } from "./todo_items"; +import { useAutoFocus } from "../utils"; + +export class TodoList extends Component { + static template = "awesome_owl.todo_list"; + static components = { TodoItem }; + setup() { + useAutoFocus("todoInput"); + this.todos = useState([]); + this.nextId = 1; + } + + addTodo(event) { + if (event.keyCode === 13) { + const description = event.target.value.trim(); + if (description) { + this.todos.push({ + id: this.nextId++, + description: description, + isCompleted: false, + }); + event.target.value = ""; + } + } + } + + toggleTodo(todoId) { + const todo = this.todos.find(t => t.id === todoId); + if (todo) { + todo.isCompleted = !todo.isCompleted; + } + } + + removeTodo(todoId) { + // Find the index of the todo to remove + const index = this.todos.findIndex(t => t.id === todoId); + if (index !== -1) { + // Remove the todo from the array + this.todos.splice(index, 1); + } + } +} diff --git a/awesome_owl/static/src/todo_list/todo_list.xml b/awesome_owl/static/src/todo_list/todo_list.xml new file mode 100644 index 00000000000..69ae97918f8 --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_list.xml @@ -0,0 +1,15 @@ + + + +
+

Todo List

+ + + + +
+
+
diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js new file mode 100644 index 00000000000..02a5eba74db --- /dev/null +++ b/awesome_owl/static/src/utils.js @@ -0,0 +1,13 @@ +/** @odoo-module **/ + +import { onMounted, useRef } from "@odoo/owl"; + +export function useAutoFocus(refname) { + const inputRef = useRef(refname) + + onMounted(() => { + if (inputRef.el) { + inputRef.el.focus(); + } + }); +} diff --git a/estate/__init__.py b/estate/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/estate/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 00000000000..2e8515d484d --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,30 @@ +{ + "name": "estate", + "description": """ + This module is used to manage the Real estate and properties. + """, + "author": "ayush", + "version": "0.1", + "application": True, + "installable": True, + "depends": ["base"], + "license": "LGPL-3", + "category": "Real Estate/Brokerage", + "data": [ + "security/estate_security.xml", + "security/ir.model.access.csv", + "views/estate_property_views.xml", + "views/estate_property_type_views.xml", + "views/estate_property_tags_views.xml", + "views/estate_property_offer_views.xml", + "views/estate_menus.xml", + "views/res_users_views.xml", + "data/estate_property_type_demo.xml", + "reports/estate_property_templates.xml", + "reports/estate_property_reports.xml", + ], + "demo": [ + "data/estate_property_demo.xml", + "data/estate_property_offer_demo.xml", + ], +} diff --git a/estate/data/estate_property_demo.xml b/estate/data/estate_property_demo.xml new file mode 100644 index 00000000000..eae66d14300 --- /dev/null +++ b/estate/data/estate_property_demo.xml @@ -0,0 +1,65 @@ + + + + Big Villa + new + A nice and big Villa + 30050 + + 1600000 + 6 + 500 + 4 + True + True + 1200 + south + + + + Ocean side Mansion + cancelled + Grand ocean side mansion with stunning views of ocean + 50052 + + 1200000 + 0 + 4 + 1000 + 4 + True + + + + Empire tower's Luxurious Penthouse + offer_received + A luxurious penthouse with views of central park + 65065 + + 1800000 + 6 + 5000 + 2 + False + False + + + + + diff --git a/estate/data/estate_property_offer_demo.xml b/estate/data/estate_property_offer_demo.xml new file mode 100644 index 00000000000..46b06f9756a --- /dev/null +++ b/estate/data/estate_property_offer_demo.xml @@ -0,0 +1,28 @@ + + + + + + 1440000 + 14 + + + + + + 1500000 + 14 + + + + + + 1550000 + 14 + + + + + + + diff --git a/estate/data/estate_property_type_demo.xml b/estate/data/estate_property_type_demo.xml new file mode 100644 index 00000000000..47f34ede85a --- /dev/null +++ b/estate/data/estate_property_type_demo.xml @@ -0,0 +1,18 @@ + + + + + + Residential + + + Commercial + + + Land + + + Industrial + + + diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..6315ba30deb --- /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 res_users diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..9279304fa42 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,169 @@ +from odoo import models, fields, api +from dateutil.relativedelta import relativedelta +from odoo.exceptions import UserError, ValidationError + + +class EstateProperty(models.Model): + _name = "estate.property" + _description = "Estate Property" + _sql_constraints = [ + ( + "estate_property_expected_price_positive", + "CHECK(expected_price > 0)", + "The expected price must be positive.", + ), + ( + "estate_property_selling_price_non_negative", + "CHECK(selling_price >= 0)", + "The selling price must be non negative.", + ), + ] + + _order = "id desc" + + name = fields.Char(string="Property Name", required=True) + description = fields.Text(string="Description") + postcode = fields.Char(string="Postcode") + date_availability = fields.Date( + string="Available From", + copy=False, + default=fields.Date.today() + relativedelta(months=4), + ) + expected_price = fields.Float(string="Expected Price") + selling_price = fields.Float(string="Selling Price", readonly=True, copy=False) + bedrooms = fields.Integer(string="Bedrooms", default=2) + living_area = fields.Integer(string="Living Area (sqm)") + facades = fields.Integer(string="Facades") + garage = fields.Boolean(string="Garage") + garden = fields.Boolean(string="Garden") + garden_area = fields.Integer(string="Garden Area (sqm)") + garden_orientation = fields.Selection( + selection=[ + ("north", "North"), + ("south", "South"), + ("east", "East"), + ("west", "West"), + ], + string="Garden Orientation", + ) + active = fields.Boolean(string="Active", default=True) + state = fields.Selection( + selection=[ + ("new", "New"), + ("offer_received", "Offer Received"), + ("offer_accepted", "Offer Accepted"), + ("sold", "Sold"), + ("cancelled", "Cancelled"), + ], + string="Status", + required=True, + copy=False, + default="new", + ) + property_type_id = fields.Many2one("estate.property.type", string="Property Type") + buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False) + salesperson_id = fields.Many2one( + "res.users", string="Salesperson", default=lambda self: self.env.user + ) + tag_ids = fields.Many2many( + "estate.property.tags", string="Tags", help="Tags for the property" + ) + offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers") + total_area = fields.Integer( + string="Total Area (sqm)", + compute="_compute_total_area", + store=True, + ) + company_id = fields.Many2one( + "res.company", required=True, default=lambda self: self.env.company + ) + best_price = fields.Float( + string="Best Price", + compute="_compute_best_price", + store=True, + ) + + @api.depends("living_area", "garden_area") + def _compute_total_area(self): + for record in self: + record.total_area = record.living_area + record.garden_area + + @api.depends("offer_ids.price") + def _compute_best_price(self): + for record in self: + if record.offer_ids: + record.best_price = max(record.offer_ids.mapped("price")) + else: + record.best_price = 0.0 + + @api.onchange("garden") + def _onchange_garden(self): + if self.garden: + self.garden_area = 10 + self.garden_orientation = "north" + else: + self.garden_area = 0 + self.garden_orientation = "" + + def action_set_sold(self): + for offer in self: + if offer.state == "sold": + raise UserError("This property is already sold.") + if offer.state == "offer_accepted": + offer.state = "sold" + return True + else: + raise UserError("Only accepted offers can set the property as sold.") + + def action_set_cancel(self): + for offer in self: + if offer.state == "new": + offer.state = "cancelled" + return True + else: + raise UserError( + "Only refused offers can set the property as cancelled." + ) + + @api.constrains("selling_price", "expected_price") + def _check_price(self): + for record in self: + if record.selling_price and record.expected_price: + if record.selling_price < 0.9 * record.expected_price: + raise ValidationError( + "The selling price must be at least 90% of the expected price." + ) + + @api.ondelete(at_uninstall=False) + def _ondelete_property(self): + for record in self: + if record.state not in ["new", "cancelled"]: + raise UserError( + "You cannot delete a property that is not new or cancelled." + ) + + @api.onchange("offer_ids") + def _onchange_offer_ids(self): + if not self.offer_ids and self.state != "new": + self.state = "new" + + @api.onchange("state") + def _onchange_state(self): + if self.state == "offer_received" and not self.offer_ids: + raise UserError("No offers available yet!.") + elif self.state == "offer_received" and self.offer_ids.filtered( + lambda o: o.status == "accepted" + ): + raise UserError( + "You cannot set the property as offer received when there is an accepted offer." + ) + elif self.state == "offer_accepted" and not self.offer_ids: + raise UserError("You cannot accept an offer without any offers.") + elif self.state == "sold" and not self.offer_ids: + raise UserError("You cannot sell a property without any offers.") + elif self.state == "offer_accepted" and not self.offer_ids.filtered( + lambda o: o.status == "accepted" + ): + raise UserError( + "You cannot set the property as offer accepted without an accepted offer." + ) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..626cdf4462e --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,133 @@ +from odoo import models, fields, api +from dateutil.relativedelta import relativedelta +from odoo.exceptions import UserError, ValidationError + + +class EstatePropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "Estate Property Offer" + _sql_constraints = [ + ( + "estate_property_offer_price_positive", + "CHECK(price > 0)", + "The offer price must be strictly positive.", + ) + ] + + _order = "price desc" + + price = fields.Float(string="Price", required=True) + status = fields.Selection( + [ + ("accepted", "Accepted"), + ("refused", "Refused"), + ], + string="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 (days)", default=7) + date_deadline = fields.Date( + string="Deadline", + compute="_compute_date_deadline", + inverse="_inverse_date_deadline", + store=True, + ) + create_date = fields.Datetime( + string="Creation Date", readonly=True, default=fields.Datetime.now + ) + property_type_id = fields.Many2one( + "estate.property.type", + string="Property Type", + related="property_id.property_type_id", + store=True, + ) + + @api.depends("create_date", "validity") + def _compute_date_deadline(self): + for offer in self: + creation_date = ( + offer.create_date.date() if offer.create_date else fields.Date.today() + ) + if offer.validity: + offer.date_deadline = creation_date + relativedelta(days=offer.validity) + else: + offer.date_deadline = creation_date + + def _inverse_date_deadline(self): + for offer in self: + creation_date = ( + offer.create_date.date() if offer.create_date else fields.Date.today() + ) + if offer.date_deadline: + offer.validity = (offer.date_deadline - creation_date).days + else: + offer.validity = 0 + + def action_accept_offer(self): + for record in self: + record_property = record.property_id + property_state = record_property.state + + if property_state == "offer_accepted": + raise UserError("You can only accept one offer at a time.") + if property_state == "sold": + raise UserError("You cannot accept an offer on a sold property.") + if property_state == "cancelled": + raise UserError("You cannot accept an offer on a cancelled property.") + + other_offers = record_property.offer_ids.filtered( + lambda o: o.id != record.id + ) + other_offers.write({"status": "refused"}) + + record.status = "accepted" + record_property.write( + { + "buyer_id": record.partner_id.id, + "selling_price": record.price, + "state": "offer_accepted", + } + ) + + return True + + def action_refuse_offer(self): + for record in self: + record_property = record.property_id + property_state = record_property.state + + if property_state in ["sold", "cancelled"]: + raise UserError( + "You cannot refuse an offer on a sold or cancelled property." + ) + if record.status == "accepted": + raise UserError("You cannot refuse an already accepted offer.") + + record.status = "refused" + return True + + @api.model_create_multi + def create(self, vals_list): + estate_property_model_instance = self.env["estate.property"] + for vals in vals_list: + property_id = vals.get("property_id") + estate_property = estate_property_model_instance.browse(property_id) + best_price = estate_property.best_price + if not estate_property: + raise ValidationError("Property not found.") + + if estate_property.state in ["sold", "cancelled"]: + raise ValidationError( + "You cannot create an offer for a sold or cancelled property." + ) + + if best_price >= vals.get("price", 0.0): + raise ValidationError( + "The offer price must be strictly higher than the previous offers." + ) + best_price = max(best_price, vals.get("price", 0.0)) + estate_property.state = "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..211c2f18d1b --- /dev/null +++ b/estate/models/estate_property_tags.py @@ -0,0 +1,18 @@ +from odoo import models, fields + + +class EstatePropertyTags(models.Model): + _name = "estate.property.tags" + _description = "Estate Property Tags" + _sql_constraints = [ + ( + "estate_property_tag_name_unique", + "UNIQUE(name)", + "The tag names must be unique.", + ) + ] + + _order = "name" + + name = fields.Char(string="Tag Name", required=True) + color = fields.Integer(string="Color Index") diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..f8b77b9a7c0 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,28 @@ +from odoo import fields, models, api + + +class EstatePropertyType(models.Model): + _name = "estate.property.type" + _description = "Real Estate Property Type" + _sql_constraints = [ + ( + "estate_property_type_name_unique", + "UNIQUE(name)", + "The type names must be unique.", + ) + ] + + _order = "sequence, name" + + name = fields.Char(required=True) + sequence = fields.Integer("Sequence") + property_ids = fields.One2many("estate.property", "property_type_id") + offer_ids = fields.One2many("estate.property.offer", "property_type_id") + offer_count = fields.Integer( + compute="_compute_offer_count", string="Offer Count", readonly=True, copy=False + ) + + @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..a051f025c06 --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,12 @@ +from odoo import models, fields + + +class ResUsers(models.Model): + _inherit = "res.users" + + property_ids = fields.One2many( + "estate.property", + "salesperson_id", + string="Properties", + domain=[("state", "in", ["new", "offer_received"])], + ) diff --git a/estate/reports/estate_property_reports.xml b/estate/reports/estate_property_reports.xml new file mode 100644 index 00000000000..3312c911e42 --- /dev/null +++ b/estate/reports/estate_property_reports.xml @@ -0,0 +1,25 @@ + + + + + Estate Property Sale Report + estate.property + qweb-pdf + estate.report_property_offers + estate.report_property_offers + 'Property Offers - ' + object.name + + report + + + + + Salesperson Properties + res.users + estate.report_salesperson_property + estate.report_salesperson_property + 'Salesperson Properties - ' + object.name + + report + + diff --git a/estate/reports/estate_property_templates.xml b/estate/reports/estate_property_templates.xml new file mode 100644 index 00000000000..acbddef08f2 --- /dev/null +++ b/estate/reports/estate_property_templates.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + diff --git a/estate/security/estate_security.xml b/estate/security/estate_security.xml new file mode 100644 index 00000000000..43a06965654 --- /dev/null +++ b/estate/security/estate_security.xml @@ -0,0 +1,44 @@ + + + + Agent + + + + + + Estate Manager + + + + + + + + Estate Property Agent Access + + + + + ['|', ('salesperson_id', '=', user.id), ('salesperson_id', '=', False)] + + + + + Estate Property Manager All Access + + + + + + + + Agents can see only their company's data + + [ + '|', ('company_id', '=', False), + ('company_id', 'in', company_ids) + ] + + + diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..eaa33aaaf2f --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,9 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_estate_property_manager,access_estate_property_manager,model_estate_property,estate_group_manager,1,1,1,1 +access_estate_property_type_manager,access_estate_property_type_manager,model_estate_property_type,estate_group_manager,1,1,1,1 +access_estate_property_tags_manager,access_estate_property_tags_manager,model_estate_property_tags,estate_group_manager,1,1,1,1 +access_estate_property_offer_manager,access_estate_property_offer_manager,model_estate_property_offer,estate_group_manager,1,1,1,1 +access_estate_property_agent,access_estate_property_agent,model_estate_property,estate_group_user,1,1,1,0 +access_estate_property_type_agent,access_estate_property_type_agent,model_estate_property_type,estate_group_user,1,0,0,0 +access_estate_property_tags_agent,access_estate_property_tags_agent,model_estate_property_tags,estate_group_user,1,0,0,0 +access_estate_property_offer_agent,access_estate_property_offer_agent,model_estate_property_offer,estate_group_user,1,1,1,0 diff --git a/estate/tests/__init__.py b/estate/tests/__init__.py new file mode 100644 index 00000000000..524f7bf1293 --- /dev/null +++ b/estate/tests/__init__.py @@ -0,0 +1 @@ +from . import estate_property_tests diff --git a/estate/tests/estate_property_tests.py b/estate/tests/estate_property_tests.py new file mode 100644 index 00000000000..b692795af5b --- /dev/null +++ b/estate/tests/estate_property_tests.py @@ -0,0 +1,79 @@ +from odoo import Command # noqa: F401 +from odoo.exceptions import UserError +from odoo.tests import tagged +from odoo.tests import Form, TransactionCase + + +@tagged("post_install", "-at_install") +class EstateTestCase(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.properties = cls.env["estate.property"].create( + [ + { + "name": "Test Property", + "description": "Some Description", + "expected_price": 100000, + "living_area": 50, + }, + { + "name": "Test Property Garden", + "description": "property with garden", + "expected_price": 200000, + "living_area": 100, + }, + ] + ) + + cls.offers = cls.env["estate.property.offer"].create( + [ + { + "partner_id": cls.env.ref("base.res_partner_2").id, + "price": 110000, + "property_id": cls.properties[0].id, + }, + { + "partner_id": cls.env.ref("base.res_partner_1").id, + "price": 120000, + "property_id": cls.properties[0].id, + }, + { + "partner_id": cls.env.ref("base.res_partner_3").id, + "price": 125000, + "property_id": cls.properties[0].id, + }, + ] + ) + + def test_property_sale(self): + with self.assertRaises(UserError): + self.properties[0].action_set_sold() + + self.offers[1].action_accept_offer() + + self.properties[0].action_set_sold() + self.assertEqual(self.properties[0].state, "sold", "Property was not sold") + + with self.assertRaises(UserError): + self.env["estate.property.offer"].create( + { + "partner_id": self.env.ref("base.res_partner_4").id, + "price": 200000, + "property_id": self.properties[0].id, + } + ) + + def test_garden_reset(self): + with Form(self.properties[1]) as form: + form.garden = True + self.assertEqual(form.garden_area, 10) + self.assertEqual(form.garden_orientation, "north") + + form.garden = False + self.assertEqual(form.garden_area, 0, "Garden area should be reset to 0") + self.assertEqual( + form.garden_orientation, + False, + "Garden orientation should be reset to False", + ) diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..c2265bae90e --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..493caa3167d --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,41 @@ + + + + estate.property.offer.tree + 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..49c56412f8b --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,110 @@ + + + + Properties + estate.property + list,form + {'search_default_available_properties': True} + + + + + estate.property.list + estate.property + + + + + + + + + + + + + + + + + + estate.property.form + estate.property + +
+
+
+ +

+ +

+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + + estate.property.search + estate.property + + + + + + + + + + + + + + + +
diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml new file mode 100644 index 00000000000..a18583359db --- /dev/null +++ b/estate/views/res_users_views.xml @@ -0,0 +1,14 @@ + + + res.users.view.form.inherit.estate + res.users + + + + + + + + + + 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..93dff616702 --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,16 @@ +{ + "name": "estate account", + "version": "0.1", + "depends": ["base", "estate", "account"], + "author": "Ayush Patel", + "category": "Real Estate", + "description": """ + This module links Estate and Accounting. + """, + "application": True, + "auto_install": True, + "data": [ + "reports/estate_account_templates.xml", + ], + "license": "LGPL-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..513e8374866 --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,31 @@ +from odoo import models, Command + + +class EstateProperty(models.Model): + _inherit = "estate.property" + + def action_set_sold(self): + 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": f"Sale of property {self.name}", + "quantity": 1, + "price_unit": self.selling_price * 0.06, + } + ), + Command.create( + { + "name": "Administrative Fees", + "quantity": 1, + "price_unit": 100.00, + } + ), + ], + } + ) + return super().action_set_sold() diff --git a/estate_account/reports/estate_account_templates.xml b/estate_account/reports/estate_account_templates.xml new file mode 100644 index 00000000000..fa9eba70534 --- /dev/null +++ b/estate_account/reports/estate_account_templates.xml @@ -0,0 +1,13 @@ + + + + diff --git a/new_product_type/__init__.py b/new_product_type/__init__.py new file mode 100644 index 00000000000..aee8895e7a3 --- /dev/null +++ b/new_product_type/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizards diff --git a/new_product_type/__manifest__.py b/new_product_type/__manifest__.py new file mode 100644 index 00000000000..d8be7483028 --- /dev/null +++ b/new_product_type/__manifest__.py @@ -0,0 +1,14 @@ +{ + "name": "New Kit Product", + "version": "1.0", + "depends": ["sale_management"], + "license": "LGPL-3", + "data": [ + "security/ir.model.access.csv", + "views/product_template_views.xml", + "views/sale_order_views.xml", + "wizards/sub_products_wizard.xml", + ], + "installable": True, + "application": True, +} diff --git a/new_product_type/models/__init__.py b/new_product_type/models/__init__.py new file mode 100644 index 00000000000..8f2f8c0cbc1 --- /dev/null +++ b/new_product_type/models/__init__.py @@ -0,0 +1,3 @@ +from . import product_template +from . import sale_order_line +from . import sale_order diff --git a/new_product_type/models/product_template.py b/new_product_type/models/product_template.py new file mode 100644 index 00000000000..d6d750f9d51 --- /dev/null +++ b/new_product_type/models/product_template.py @@ -0,0 +1,13 @@ +from odoo import models, fields + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + is_kit = fields.Boolean(string="Is Kit", default=False) + sub_products = fields.Many2many( + "product.product", + string="Sub Products", + help="Select the products that are part of this kit", + domain="[('is_kit', '=', False)]", + ) diff --git a/new_product_type/models/sale_order.py b/new_product_type/models/sale_order.py new file mode 100644 index 00000000000..37a6f0466be --- /dev/null +++ b/new_product_type/models/sale_order.py @@ -0,0 +1,41 @@ +from odoo import api, fields, models + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + print_in_report = fields.Boolean( + string="Print Sub-products in Report", + default=False, + help="If checked, the individual sub-product components will be printed on the quotation/order report.", + ) + + @api.onchange("order_line") + def _onchange_order_line(self): + current_kit_ids = [ + line._origin.id for line in self.order_line if line.product_is_kit + ] + + new_order_lines = self.order_line.filtered( + lambda line: not line.parent_kit_line_id.id + or (line.parent_kit_line_id.id in current_kit_ids) + ) + + self.order_line = new_order_lines + + def _get_order_lines_to_report(self): + order_lines = super()._get_order_lines_to_report() + if self.print_in_report: + return order_lines + else: + return order_lines.filtered(lambda line: not line.parent_kit_line_id) + + def _get_invoiceable_lines(self, final=False): + invoicable_lines = super()._get_invoiceable_lines(final=final) + print(len(invoicable_lines), "invoicable lines before filter") + if self.print_in_report: + print(len(invoicable_lines), "invoicable lines after filter if true") + return invoicable_lines + else: + print(len(invoicable_lines), "invoicable lines after filter") + return invoicable_lines.filtered(lambda line: not line.parent_kit_line_id) diff --git a/new_product_type/models/sale_order_line.py b/new_product_type/models/sale_order_line.py new file mode 100644 index 00000000000..86c43d28291 --- /dev/null +++ b/new_product_type/models/sale_order_line.py @@ -0,0 +1,32 @@ +from odoo import api, fields, models + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + product_is_kit = fields.Boolean( + related="product_id.product_tmpl_id.is_kit", + ) + + parent_kit_line_id = fields.Many2one( + "sale.order.line", + string="Parent Kit Line", + ondelete="cascade", + copy=False, + ) + + sub_product_line_ids = fields.One2many( + "sale.order.line", "parent_kit_line_id", string="Sub-product Lines", copy=False + ) + + is_kit_sub_product = fields.Boolean(string="Is a Kit Sub-product", copy=False) + + def open_sub_product_wizard(self): + return { + "name": f"Product : {self.product_id.display_name}", + "type": "ir.actions.act_window", + "res_model": "sub.products.wizard", + "view_mode": "form", + "target": "new", + "context": {"active_id": self.id}, + } diff --git a/new_product_type/security/ir.model.access.csv b/new_product_type/security/ir.model.access.csv new file mode 100644 index 00000000000..6a08720d626 --- /dev/null +++ b/new_product_type/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 +access_sub_products_wizard_user,access.sub.products.wizard.user,model_sub_products_wizard,base.group_user,1,1,1,1 +access_sub_products_line_wizard_user,access.sub.products.line.wizard.user,model_sub_products_line_wizard,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/new_product_type/views/product_template_views.xml b/new_product_type/views/product_template_views.xml new file mode 100644 index 00000000000..dcd8d4f59e8 --- /dev/null +++ b/new_product_type/views/product_template_views.xml @@ -0,0 +1,13 @@ + + + product.template.form.kit.inherit + product.template + + + + + + + + + \ No newline at end of file diff --git a/new_product_type/views/sale_order_views.xml b/new_product_type/views/sale_order_views.xml new file mode 100644 index 00000000000..794e87a314c --- /dev/null +++ b/new_product_type/views/sale_order_views.xml @@ -0,0 +1,40 @@ + + + + sale.order.form.inherited + sale.order + + + + + + + + + + +
+
diff --git a/odoo_self_order_details/views/product_template_view.xml b/odoo_self_order_details/views/product_template_view.xml new file mode 100644 index 00000000000..aff114f9b2e --- /dev/null +++ b/odoo_self_order_details/views/product_template_view.xml @@ -0,0 +1,15 @@ + + + + + product.template.form.inherit.self.order + product.template + + + + + + + + + diff --git a/purchase_order_print/__init__.py b/purchase_order_print/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/purchase_order_print/__manifest__.py b/purchase_order_print/__manifest__.py new file mode 100644 index 00000000000..00c4f037fbc --- /dev/null +++ b/purchase_order_print/__manifest__.py @@ -0,0 +1,19 @@ +{ + "name": "Purchase Order Print", + "version": "1.0", + "summary": "Custom module to print purchase orders with additional fields.", + "author": "Ayush Patel", + "depends": ["purchase", "base", "hr"], + "license": "LGPL-3", + "data": [ + "reports/custom_purchase_report_template.xml", + "views/hr_employee_model_fields.xml", + "views/hr_employee_views.xml", + "views/product_model_fields.xml", + "views/product_template_views.xml", + "views/purchase_order_model_fields.xml", + "views/purchase_order_views.xml", + ], + "installable": True, + "application": True, +} \ No newline at end of file diff --git a/purchase_order_print/reports/custom_purchase_report_template.xml b/purchase_order_print/reports/custom_purchase_report_template.xml new file mode 100644 index 00000000000..3ee5e14f1e1 --- /dev/null +++ b/purchase_order_print/reports/custom_purchase_report_template.xml @@ -0,0 +1,259 @@ + + + + + \ No newline at end of file diff --git a/purchase_order_print/views/hr_employee_model_fields.xml b/purchase_order_print/views/hr_employee_model_fields.xml new file mode 100644 index 00000000000..7156b717b24 --- /dev/null +++ b/purchase_order_print/views/hr_employee_model_fields.xml @@ -0,0 +1,11 @@ + + + + x_signature_seal_image + hr.employee + + Signature/Seal Image + binary + True + + diff --git a/purchase_order_print/views/hr_employee_views.xml b/purchase_order_print/views/hr_employee_views.xml new file mode 100644 index 00000000000..35c19beb9a0 --- /dev/null +++ b/purchase_order_print/views/hr_employee_views.xml @@ -0,0 +1,15 @@ + + + + hr.employee.form.inherit.signature.seal + hr.employee + + + + + + + + + + \ No newline at end of file diff --git a/purchase_order_print/views/product_model_fields.xml b/purchase_order_print/views/product_model_fields.xml new file mode 100644 index 00000000000..25c4c461e1a --- /dev/null +++ b/purchase_order_print/views/product_model_fields.xml @@ -0,0 +1,31 @@ + + + + + x_part_no + product.template + + char + Part No. + manual + + + + x_packing + product.template + + float + Packing + manual + + + + x_freight + product.template + + float + Freight + manual + + + \ No newline at end of file diff --git a/purchase_order_print/views/product_template_views.xml b/purchase_order_print/views/product_template_views.xml new file mode 100644 index 00000000000..85f43cdba92 --- /dev/null +++ b/purchase_order_print/views/product_template_views.xml @@ -0,0 +1,16 @@ + + + product.template.form.inherit.partno.packing.freight + product.template + + + + + + + + + + + + \ No newline at end of file diff --git a/purchase_order_print/views/purchase_order_model_fields.xml b/purchase_order_print/views/purchase_order_model_fields.xml new file mode 100644 index 00000000000..a13915bfbaa --- /dev/null +++ b/purchase_order_print/views/purchase_order_model_fields.xml @@ -0,0 +1,20 @@ + + + + + x_employee_id + + many2one + hr.employee + Authorized By + manual + + + x_annexure_terms + + html + Annexure terms + manual + + + \ No newline at end of file diff --git a/purchase_order_print/views/purchase_order_views.xml b/purchase_order_print/views/purchase_order_views.xml new file mode 100644 index 00000000000..aa14a62acd9 --- /dev/null +++ b/purchase_order_print/views/purchase_order_views.xml @@ -0,0 +1,29 @@ + + + + + purchase.order.form.inherit.employee + purchase.order + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sale_person/__init__.py b/sale_person/__init__.py new file mode 100644 index 00000000000..366188d46b3 --- /dev/null +++ b/sale_person/__init__.py @@ -0,0 +1 @@ +# Odoo module marker \ No newline at end of file diff --git a/sale_person/__manifest__.py b/sale_person/__manifest__.py new file mode 100644 index 00000000000..4d9c8cc1b35 --- /dev/null +++ b/sale_person/__manifest__.py @@ -0,0 +1,20 @@ +{ + "name": "Sale Person Attendance", + "version": "1.0", + "summary": "Track salesperson attendance and customer visits.", + "author": "Ayush Patel", + "depends": ["base", "base_automation"], + "license": "LGPL-3", + "data": [ + "views/tag_model_fields.xml", + "views/contact_model_fields.xml", + "views/sale_person_model_fields.xml", + "views/tag_views.xml", + "views/contact_views.xml", + "views/sale_person_views.xml", + "views/sale_person_menu.xml", + "security/ir.model.access.csv", + ], + "installable": True, + "application": True, +} diff --git a/sale_person/security/ir.model.access.csv b/sale_person/security/ir.model.access.csv new file mode 100644 index 00000000000..52d25b5b373 --- /dev/null +++ b/sale_person/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 +access_x_contact,x_contact,model_x_contact,base.group_user,1,1,1,1 +access_x_sale_person,x_sale_person,model_x_sale_person,base.group_user,1,1,1,1 +access_x_sale_person_tag,x_sale_person_tag,model_x_sale_person_tag,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/sale_person/views/contact_model_fields.xml b/sale_person/views/contact_model_fields.xml new file mode 100644 index 00000000000..546b39a127e --- /dev/null +++ b/sale_person/views/contact_model_fields.xml @@ -0,0 +1,53 @@ + + + + + + + Contact + x_contact + manual + + + + + + + + x_name + char + Name + + + + + + x_customer_type + char + Customer Type + + + + + + x_city + char + City + + + + + + x_area + char + Area + + + + + + x_pin_code + char + Pin Code + + \ No newline at end of file diff --git a/sale_person/views/contact_views.xml b/sale_person/views/contact_views.xml new file mode 100644 index 00000000000..7e5fafaeac2 --- /dev/null +++ b/sale_person/views/contact_views.xml @@ -0,0 +1,49 @@ + + + + + + + Contacts + x_contact + list,form + + + + + x.contact.form + x_contact + +
+ + + + + + + + + + + + + +
+
+
+ + + + x.contact.list + x_contact + + + + + + + + + + +
\ No newline at end of file diff --git a/sale_person/views/sale_person_menu.xml b/sale_person/views/sale_person_menu.xml new file mode 100644 index 00000000000..f8744d13267 --- /dev/null +++ b/sale_person/views/sale_person_menu.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/sale_person/views/sale_person_model_fields.xml b/sale_person/views/sale_person_model_fields.xml new file mode 100644 index 00000000000..fba72ac567c --- /dev/null +++ b/sale_person/views/sale_person_model_fields.xml @@ -0,0 +1,173 @@ + + + + + + + Sale Person + x_sale_person + manual + + + + + + + x_user_id + + many2one + res.users + 1 + restrict + + + + + x_check_in + + datetime + Check-in Time + + + + + x_check_out + + datetime + Check-out Time + + + + + x_customer_id + + many2one + x_contact + 1 + restrict + Customer + + + + + x_city + + char + City + x_customer_id.x_city + 1 + + + + x_area + + char + Area + x_customer_id.x_area + 1 + + + + x_pin_code + + char + Pin Code + x_customer_id.x_pin_code + 1 + + + + + x_agenda + + char + Agenda + + + + + x_conversion_possibility + + selection + [('high','High'),('moderate','Moderate'),('low','Low')] + Conversion Possibility + + + + + x_worked_hours + + float + Worked Hours + + + + + x_tag_ids + + many2many + x_sale_person_tag + Tags + + + + + x_checkin_location + + char + Check-in Location + + + + + x_checkout_location + + char + Check-out Location + + + + + Check Out + + code + +for record in records: + if not record.x_check_out: + checkout_time = datetime.datetime.now() + vals = { + 'x_check_out': checkout_time, + 'x_checkout_location': 'Auto-detected location (Checkout)' + } + if record.x_check_in: + delta = checkout_time - record.x_check_in + vals['x_worked_hours'] = delta.total_seconds() / 3600.0 + record.write(vals) + + + + + + Auto Set Check In Time + + code + +record.write( + { + 'x_check_in': datetime.datetime.now(), + 'x_checkin_location': 'Auto-detected location (checkin)' + } +) + + + + + + Set Check In Time on Create + + on_change + + + 1 + + \ No newline at end of file diff --git a/sale_person/views/sale_person_views.xml b/sale_person/views/sale_person_views.xml new file mode 100644 index 00000000000..4c413d799c4 --- /dev/null +++ b/sale_person/views/sale_person_views.xml @@ -0,0 +1,76 @@ + + + + + + + Sales Person Attendance + x_sale_person + list,form + {'default_x_user_id': uid} + + + + + x.sale.person.list + x_sale_person + + + + + + + + + + + + + + + + x.sale.person.form + x_sale_person + +
+
+ +
+ +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
\ No newline at end of file diff --git a/sale_person/views/tag_model_fields.xml b/sale_person/views/tag_model_fields.xml new file mode 100644 index 00000000000..0d3ff963d01 --- /dev/null +++ b/sale_person/views/tag_model_fields.xml @@ -0,0 +1,30 @@ + + + + + + + x_Sale Person Tag + x_sale_person_tag + manual + + + + + + + x_name + + char + Tag Name + 1 + + + + + x_color + + char + Color + + \ No newline at end of file diff --git a/sale_person/views/tag_views.xml b/sale_person/views/tag_views.xml new file mode 100644 index 00000000000..b9d575a56b7 --- /dev/null +++ b/sale_person/views/tag_views.xml @@ -0,0 +1,39 @@ + + + + + + + Tags + x_sale_person_tag + list,form + + + + + x.sale.person.tag.form + x_sale_person_tag + +
+ + + + + + +
+
+
+ + + + x.sale.person.tag.list + x_sale_person_tag + + + + + + + +
\ No newline at end of file