diff --git a/appointment_filters/__init__.py b/appointment_filters/__init__.py new file mode 100644 index 00000000000..e046e49fbe2 --- /dev/null +++ b/appointment_filters/__init__.py @@ -0,0 +1 @@ +from . import controllers diff --git a/appointment_filters/__manifest__.py b/appointment_filters/__manifest__.py new file mode 100644 index 00000000000..d3b0381e33c --- /dev/null +++ b/appointment_filters/__manifest__.py @@ -0,0 +1,13 @@ +{ + 'name': 'Appointment Filters', + 'version': '18.0', + 'description': 'Appointment Filters', + 'depends': ['website_appointment','appointment_account_payment'], + 'author': 'Rahul Jha (jhra)', + 'website': 'https://www.odoo.com', + 'category': 'Appointment/Filters', + 'data': [ + 'views/appointment_filters.xml' + ], + 'license': 'LGPL-3', +} diff --git a/appointment_filters/controllers/__init__.py b/appointment_filters/controllers/__init__.py new file mode 100644 index 00000000000..12a7e529b67 --- /dev/null +++ b/appointment_filters/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/appointment_filters/controllers/main.py b/appointment_filters/controllers/main.py new file mode 100644 index 00000000000..f7b8324da97 --- /dev/null +++ b/appointment_filters/controllers/main.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +from odoo import http, _ +from odoo.addons.website_appointment.controllers.appointment import WebsiteAppointment +from odoo.http import request + +class WebsiteAppointmentFiltersController(WebsiteAppointment): + @http.route(['/appointment'], type='http', auth='public', website=True) + def appointment_type_index(self, **kwargs): + response = super().appointment_type_index() + + # Get all initial appointment types from the parent method + all_appointment_types = response.qcontext.get('appointment_types', request.env['appointment.type'].sudo()) + + # Extract filter parameters + filter_type = kwargs.get('type') + filter_payment = kwargs.get('payment_step') + filter_schedule = kwargs.get('schedule') + + # Start with the base domain - maintaining compatibility with parent controller + domain = [('website_published', '=', True)] + filter_descriptions = [] + + # Apply type filter (online/offline) + if filter_type == 'online': + domain.append(('location_id', '=', False)) + filter_descriptions.append(_("Online appointments")) + elif filter_type == 'offline': + domain.append(('location_id', '!=', False)) + filter_descriptions.append(_("Offline appointments")) + + # Apply payment filter + if filter_payment == 'paid': + domain.append(('has_payment_step', '=', True)) + filter_descriptions.append(_("Paid appointments")) + elif filter_payment == 'free': + domain.append(('has_payment_step', '=', False)) + filter_descriptions.append(_("Free appointments")) + + # Apply schedule filter + if filter_schedule == 'users': + domain.append(('schedule_based_on', '=', 'users')) + filter_descriptions.append(_("User-based scheduling")) + elif filter_schedule == 'resources': + domain.append(('schedule_based_on', '=', 'resources')) + filter_descriptions.append(_("Resource-based scheduling")) + + # Get filtered appointment types if any filters are applied + total_count = len(all_appointment_types) + filtered_count = None + if len(domain) > 1: # More than just the website_published filter + filtered_types = request.env['appointment.type'].sudo().search(domain) + response.qcontext['appointment_types'] = filtered_types + filtered_count = len(filtered_types) + + # Add filter information to context + has_filters = bool(filter_type and filter_type != 'all' or + filter_payment and filter_payment != 'all' or + filter_schedule and filter_schedule != 'all') + + # Format the active filters text for display + active_filters_text = [] + if filter_descriptions: + active_filters_text.extend(filter_descriptions) + print("active_filters_text", active_filters_text) + + response.qcontext.update({ + 'active_filters': filter_descriptions, + 'current_filters': { + 'type': filter_type, + 'payment_step': filter_payment, + 'schedule': filter_schedule + }, + 'has_filters': has_filters, + 'filtered_count': filtered_count, + 'total_count': total_count, + 'print_active_filters': active_filters_text, + }) + + return response diff --git a/appointment_filters/views/appointment_filters.xml b/appointment_filters/views/appointment_filters.xml new file mode 100644 index 00000000000..58e111e149c --- /dev/null +++ b/appointment_filters/views/appointment_filters.xml @@ -0,0 +1,194 @@ + + + + + + + + + diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py index 31406e8addb..4dcfb445910 100644 --- a/awesome_dashboard/__manifest__.py +++ b/awesome_dashboard/__manifest__.py @@ -25,6 +25,9 @@ 'web.assets_backend': [ 'awesome_dashboard/static/src/**/*', ], + 'awesome_dashboard.dashboard': [ + 'awesome_dashboard/static/src/dashboard/**/*', + ] }, 'license': 'AGPL-3' } diff --git a/awesome_dashboard/static/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..c7ce195166a --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -0,0 +1,96 @@ +/** @odoo-module **/ + +import { Component, useState, onWillStart } 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 { loadStatistics } from "./statistics"; +import { Dialog } from "@web/core/dialog/dialog"; +import { CheckBox } from "@web/core/checkbox/checkbox"; +import { browser } from "@web/core/browser/browser"; + +class AwesomeDashboard extends Component { + static template = "awesome_dashboard.AwesomeDashboard"; + static components = { Layout, DashboardItem }; + + + setup() { + this.action = useService("action"); + this.orderStats = useState({value: null}); + onWillStart(this.onWillStart); + this.dialog = useService( "dialog"); + this.state = useState({ + disabledItems: browser.localStorage.getItem("disabledDashboardItems")?.split(",") || [] + }); + } + + async onWillStart() { + this.orderStats.value = await loadStatistics.start(); + console.log(this.orderStats.value); + this.items = registry.category("awesome_dashboard").getAll(); + } + + openConfiguration() { + this.dialog.add(ConfigurationDialog, { + items: this.items, + disabledItems: this.state.disabledItems, + onUpdateConfiguration: this.updateConfiguration.bind(this), + }) + } + + updateConfiguration(newDisabledItems) { + this.state.disabledItems = newDisabledItems; + } + + openCustomerKanban() { + this.action.doAction("base.action_partner_form"); + } + + openLeads() { + this.action.doAction({ + type: "ir.actions.act_window", + name: "All Leads", + res_model: "crm.lead", + views: [[false, "list"], [false, "form"]], + }); + } + + +} + +class ConfigurationDialog extends Component { + static template = "awesome_dashboard.ConfigurationDialog"; + static components = { Dialog, CheckBox }; + static props = ["close", "items", "disabledItems", "onUpdateConfiguration"]; + + setup() { + this.items = useState(this.props.items.map((item) => { + return { + ...item, + enabled: !this.props.disabledItems.includes(item.id), + } + })); + } + + done() { + this.props.close(); + } + + onChange(checked, changedItem) { + changedItem.enabled = checked; + const newDisabledItems = Object.values(this.items).filter( + (item) => !item.enabled + ).map((item) => item.id) + + browser.localStorage.setItem( + "disabledDashboardItems", + newDisabledItems, + ); + + this.props.onUpdateConfiguration(newDisabledItems); + } + +} + +registry.category("lazy_components").add("AwesomeDashboard", AwesomeDashboard); diff --git a/awesome_dashboard/static/src/dashboard/dashboard.scss b/awesome_dashboard/static/src/dashboard/dashboard.scss new file mode 100644 index 00000000000..ae651771026 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.scss @@ -0,0 +1,33 @@ +.o_dashboard { + background-color: #575757; +} + +.title { + font-size: 18px; + font-weight: bold; +} + +.card { + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + padding: 15px; + transition: transform 0.2s; + overflow: hidden; +} + +.card:hover { + transform: scale(1.05); +} + +/* Responsive design */ +@media (max-width: 768px) { + .card { + grid-column: span 2; + } +} + +@media (max-width: 480px) { + .card { + grid-column: span 1; + } +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml new file mode 100644 index 00000000000..085dc6b1326 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + +
+ + + + + + +
+
+
+ + + + Which cards do you whish to see ? + + + + + + + + + + + +
diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js new file mode 100644 index 00000000000..ef44404f474 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js @@ -0,0 +1,25 @@ +/** @odoo-module **/ + +import { Component } from "@odoo/owl"; + +export class DashboardItem extends Component { + static template = "awesome_dashboard.DashboardItem" + static props = { + slots: { + type: Object, + shape: { + default: Object + }, + }, + + size: { + type: Number, + optional: true, + }, + } + + static defaultProps = { + size: 1, + + }; +} \ No newline at end of file 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..d023340bb59 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml @@ -0,0 +1,10 @@ + + + +
+
+ +
+
+
+
\ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/dashboard_items.js b/awesome_dashboard/static/src/dashboard/dashboard_items.js new file mode 100644 index 00000000000..3ec5145b5ee --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_items.js @@ -0,0 +1,65 @@ +import { NumberCard } from "./number_card/number_card"; +import { PieChartCard } from "./piechart_card/piechart_card"; +import { registry } from "@web/core/registry"; + +const items = [ + { + id: "average_quantity", + description: "Average amount of t-shirt", + Component: NumberCard, + props: (data) => ({ + title: "Average amount of t-shirt by order", + value: data.average_quantity, + }) + }, + { + id: "number_new_orders", + description: "New orders this month", + Component: NumberCard, + props: (data) => ({ + title: "Number of new orders", + value: data.nb_new_orders, + }) + }, + { + id: "amount_new_orders", + description: "amount orders this month", + Component: NumberCard, + props: (data) => ({ + title: "Total amount of new orders", + value: data.total_amount, + }) + }, + { + id: "cancelled_orders", + description: "Cancelled orders this month", + Component: NumberCard, + props: (data) => ({ + title: "Number of cancelled orders", + value: data.nb_cancelled_orders, + }) + }, + { + id: "average_time", + description: "Average time for an order", + Component: NumberCard, + props: (data) => ({ + title: "Average time for an order to go from 'new' to 'sent' or 'cancelled'", + value: data.average_time, + }) + }, + { + id: "pie_chart", + description: "Shirt orders by size", + Component: PieChartCard, + size: 2, + props: (data) => ({ + title: "Shirt orders by size", + values: data.orders_by_size, + }) + } +] + +items.forEach((item) => { + registry.category("awesome_dashboard").add(item.id,item); +}); diff --git a/awesome_dashboard/static/src/dashboard/number_card/number_card.js b/awesome_dashboard/static/src/dashboard/number_card/number_card.js new file mode 100644 index 00000000000..ca8b584a334 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/number_card/number_card.js @@ -0,0 +1,13 @@ +import { Component } from "@odoo/owl"; + +export class NumberCard extends Component { + static template = "awesome_dashboard.NumberCard"; + static props = { + title: { + type: String, + }, + value: { + type: Number, + } + } +} 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..5c9a6752946 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/number_card/number_card.xml @@ -0,0 +1,11 @@ + + + +

+ +

+
+ +
+
+
diff --git a/awesome_dashboard/static/src/dashboard/piechart/piechart.js b/awesome_dashboard/static/src/dashboard/piechart/piechart.js new file mode 100644 index 00000000000..119de04dcba --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/piechart/piechart.js @@ -0,0 +1,53 @@ +import { loadJS } from "@web/core/assets"; +import { getColor } from "@web/core/colors/colors"; +import { Component, onWillStart, useRef, onMounted, onWillUnmount, onWillUpdateProps } from "@odoo/owl"; + +export class PieChart extends Component { + static template = "awesome_dashboard.PieChart"; + static props = { + label: String, + data: Object, + }; + + setup() { + this.canvasRef = useRef("canvas"); + onWillStart(() => loadJS("/web/static/lib/Chart/Chart.js")); + onMounted(() => { + this.renderChart(); + }); + onWillUnmount(() => { + this.chart.destroy(); + }); + onWillUpdateProps((nextProps) => { + this.updateChart(nextProps.data); + }); + } + + renderChart() { + const labels = Object.keys(this.props.data); + const data = Object.values(this.props.data); + const color = labels.map((_, index) => getColor(index)); + + this.chart = new Chart(this.canvasRef.el, { + type: "pie", + data: { + labels: labels, + datasets: [ + { + label: this.props.label, + data: data, + backgroundColor: color, + }, + ], + }, + }); + } + + updateChart(newData) { + if (this.chart) { + this.chart.data.labels = Object.keys(newData); + this.chart.data.datasets[0].data = Object.values(newData); + this.chart.update(); + } + } +} diff --git a/awesome_dashboard/static/src/dashboard/piechart/piechart.xml b/awesome_dashboard/static/src/dashboard/piechart/piechart.xml new file mode 100644 index 00000000000..e1d0edacafd --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/piechart/piechart.xml @@ -0,0 +1,10 @@ + + + + +
+ +
+
+ +
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..65a4499c51c --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/piechart_card/piechart_card.js @@ -0,0 +1,17 @@ +/** @odoo-module */ + +import { Component } from "@odoo/owl"; +import { PieChart } from "../piechart/piechart"; + +export class PieChartCard extends Component { + static template = "awesome_dashboard.PieChartCard"; + static components = { PieChart } + static props = { + title: { + type: String, + }, + values: { + 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..cbc788f2125 --- /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.js b/awesome_dashboard/static/src/dashboard/statistics.js new file mode 100644 index 00000000000..62deaabd50c --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/statistics.js @@ -0,0 +1,23 @@ +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"; + +export const loadStatistics = { + + start: function () { + const stats = reactive({isReady: false}); + + async function loadData() { + const updates = await rpc("/awesome_dashboard/statistics"); + Object.assign(stats, updates, { isReady: true }); + } + + setInterval(loadData, 100000); + loadData(); + + return stats; + } +} + +registry.category("services").add("awesome_dashboard.statistics", loadStatistics); diff --git a/awesome_dashboard/static/src/dashboard_action.js b/awesome_dashboard/static/src/dashboard_action.js new file mode 100644 index 00000000000..f73302922da --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_action.js @@ -0,0 +1,15 @@ +/** @odoo-module **/ + +import {registry} from "@web/core/registry"; +import {Component, xml} from "@odoo/owl"; +import {LazyComponent} from "@web/core/assets"; + + +class AwesomeDashboardAction extends Component { + static components = { LazyComponent }; + static template = xml` + + `; +} + +registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboardAction); diff --git a/awesome_owl/controllers/__init__.py b/awesome_owl/controllers/__init__.py index 457bae27e11..65a8c12013d 100644 --- a/awesome_owl/controllers/__init__.py +++ b/awesome_owl/controllers/__init__.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- -from . import controllers \ No newline at end of file +from . import main diff --git a/awesome_owl/controllers/controllers.py b/awesome_owl/controllers/main.py similarity index 80% rename from awesome_owl/controllers/controllers.py rename to awesome_owl/controllers/main.py index bccfd6fe283..f315db02be9 100644 --- a/awesome_owl/controllers/controllers.py +++ b/awesome_owl/controllers/main.py @@ -2,7 +2,7 @@ from odoo.http import request, route class OwlPlayground(http.Controller): - @http.route(['/awesome_owl'], type='http', auth='public') + @http.route(['/awesome-owl'], type='http', auth='public') def show_playground(self): """ Renders the owl playground page diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js new file mode 100644 index 00000000000..1c2f263c553 --- /dev/null +++ b/awesome_owl/static/src/card/card.js @@ -0,0 +1,25 @@ +/** @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 }, + slots: { type: Object, shape: { + default: true + }}, + } + setup() { + this.title = this.props.title; + this.isOpen = useState({value: false}); + } + + onClick() { + console.log(`Card ${this.title} clicked`) + } + + toggleOpen() { + this.isOpen.value = !this.isOpen.value; + } +} diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml new file mode 100644 index 00000000000..3e830cdf223 --- /dev/null +++ b/awesome_owl/static/src/card/card.xml @@ -0,0 +1,12 @@ + + + +
+
+
+ +

+
+
+
+
diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js new file mode 100644 index 00000000000..159d858b9b5 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.js @@ -0,0 +1,52 @@ +/** @odoo-module **/ + +import { Component, useState } from "@odoo/owl"; +// import { sharedCounter } from "../store"; + +export class Counter extends Component { + static template = "awesome_owl.counter" + + static props = { + onChange: { type: Function, optional: true }, + } + + setup() { + this.state = useState({ + value: 0, + }) + } + + increment() { + this.state.value++ + if (this.props.onChange) { + this.props.onChange(this.state.value) + } + } +} + + +// export class Counter extends Component { +// static template = "awesome_owl.counter"; +// static props = { +// id: { type: Number }, +// }; + +// setup() { +// this.state = useState(sharedCounter); +// } + +// increment() { +// if (this.props.id === 1) { +// // Counter 1 only increments itself +// this.state.counter1++; +// } else if (this.props.id === 2) { +// // Counter 2 increments both counters +// this.state.counter1++; +// this.state.counter2++; +// } +// } + +// get value() { +// return this.props.id === 1 ? this.state.counter1 : this.state.counter2; +// } +// } diff --git a/awesome_owl/static/src/counter/counter.xml b/awesome_owl/static/src/counter/counter.xml new file mode 100644 index 00000000000..4f21286ea40 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.xml @@ -0,0 +1,19 @@ + + + +
+

Counter:

+ +
+
+
+ + diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 657fb8b07bb..043b6dcaa75 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,7 +1,14 @@ /** @odoo-module **/ -import { Component } from "@odoo/owl"; +import { Component, markup , useState} from "@odoo/owl"; +import { Card } from "./card/card"; +import { Counter } from "./counter/counter"; export class Playground extends Component { static template = "awesome_owl.playground"; + + static components = { + Card, + Counter + } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..196eaf1774a 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -1,10 +1,15 @@ - -
- hello world +
+
+ + + + + + +
- diff --git a/awesome_owl/static/src/store.js b/awesome_owl/static/src/store.js new file mode 100644 index 00000000000..ba5f7fed81d --- /dev/null +++ b/awesome_owl/static/src/store.js @@ -0,0 +1,6 @@ +/** @odoo-module **/ + +export const sharedCounter = { + counter1: 0, + counter2: 0 +} 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..ebe398c5c33 --- /dev/null +++ b/awesome_owl/static/src/todo/todo_item.js @@ -0,0 +1,28 @@ +/** @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 }, + description: { type: String }, + isCompleted: { type: Boolean }, + }, + }, + toggleState: { type: Function }, + removeTodo: { type: Function }, + } + + onChange() { + this.props.toggleState(this.props.todo.id); + } + + onRemove() { + this.props.removeTodo(this.props.todo.id); + } +} 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..784c4c0b59f --- /dev/null +++ b/awesome_owl/static/src/todo/todo_item.xml @@ -0,0 +1,12 @@ + + + +
+ + +
+
+
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..ab383f31d8b --- /dev/null +++ b/awesome_owl/static/src/todo/todo_list.js @@ -0,0 +1,36 @@ +/** @odoo-module **/ + +import { Component, useState, useRef } from "@odoo/owl"; +import { TodoItem } from "./todo_item"; +import { useAutofocus } from "../utils"; + +export class TodoList extends Component { + static template = "awesome_owl.todo_list"; + + static components = { + TodoItem, + } + + setup() { + this.todos = useState([]); + this.nextId = 0; + useAutofocus("input"); + } + + addTodo(event) { + if (event.key == "Enter" && event.target.value!="") { + this.todos.push({ id: this.nextId++, description: event.target.value, isCompleted: false }); + event.target.value = ""; + } + } + toggleTodo(id) { + const todo = this.todos.find((todo) => todo.id === id); + todo.isCompleted = !todo.isCompleted; + } + removeTodo(id) { + const index = this.todos.findIndex((todo) => todo.id === id); + if (index >= 0) { + this.todos.splice(index, 1); + } + } +} 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..d5818d86716 --- /dev/null +++ b/awesome_owl/static/src/todo/todo_list.xml @@ -0,0 +1,11 @@ + + + +
+ + + + +
+
+
diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js new file mode 100644 index 00000000000..a4e5924280a --- /dev/null +++ b/awesome_owl/static/src/utils.js @@ -0,0 +1,11 @@ +import { onMounted, useRef } from "@odoo/owl"; + +export function useAutofocus(refName) { + if (!refName) { + return; + } + const ref = useRef(refName); + onMounted(() => { + ref.el.focus(); + }); +} \ No newline at end of file diff --git a/estate/__init__.py b/estate/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/estate/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 00000000000..1b2fbbe5e12 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,31 @@ +{ + 'name': "Real Estate", + 'version': '18.0', + 'depends': ['base'], + 'author': "Rahul Jha (jhra)", + 'website': "https://www.odoo.com", + 'category': 'Real Estate/Brokerage', + 'description': """ + Test module for selling real estate properties. + """, + 'data': [ + 'security/estate_security.xml', + 'security/ir.model.access.csv', + 'report/estate_property_reports.xml', + 'report/estate_property_template.xml', + 'views/estate_property_offer_views.xml', + 'views/estate_property_tag_views.xml', + 'views/estate_property_type_views.xml', + 'views/estate_property_views.xml', + 'views/estate_menus.xml', + 'views/res_users_views.xml', + ], + 'demo': [ + 'demo/estate.property.type.csv', + 'demo/estate_property_demo.xml', + 'demo/property_with_offers.xml', + ], + 'application': True, + 'installable': True, + 'license': 'LGPL-3' +} diff --git a/estate/demo/estate.property.type.csv b/estate/demo/estate.property.type.csv new file mode 100644 index 00000000000..d09052c3d24 --- /dev/null +++ b/estate/demo/estate.property.type.csv @@ -0,0 +1,4 @@ +id,name +id1,Commercial +id2,Industrial +id3,Land diff --git a/estate/demo/estate_property_demo.xml b/estate/demo/estate_property_demo.xml new file mode 100644 index 00000000000..d6862cc8eb3 --- /dev/null +++ b/estate/demo/estate_property_demo.xml @@ -0,0 +1,69 @@ + + + + + Residential + + + + Big Villa + new + A nice and big villa + + 12345 + 2020-02-02 + 1600000 + 1 + 6 + 100 + 4 + 1 + 1 + 100000 + south + + + + Trailer home + canceled + Home in a trailer park + + 54321 + 1970-01-01 + 100000 + 120000 + 1 + 10 + 4 + 0 + + + + + + 10000 + 14 + + + + + + + 1500000 + 14 + + + + + + + 1500001 + 14 + + + + + + + + diff --git a/estate/demo/property_with_offers.xml b/estate/demo/property_with_offers.xml new file mode 100644 index 00000000000..8cf35518ffa --- /dev/null +++ b/estate/demo/property_with_offers.xml @@ -0,0 +1,24 @@ + + + + Luxury Apartment + A high-end apartment in the city center + 98765 + 2025-06-01 + 500000 + 0.1 + 3 + 120 + 2 + 1 + 0 + new + + + + + + diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..9a2189b6382 --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1,5 @@ +from . import estate_property +from . import estate_property_type +from . import estate_property_tag +from . import estate_property_offer +from . import res_users diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..d21ba74eb5e --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,98 @@ +from odoo import models, fields, api, _ +from odoo.exceptions import UserError + + +class EstateProperty(models.Model): + _name = 'estate.property' + _description = "Estate Property" + _order = "id desc" + + name = fields.Char(required=True) + description = fields.Text() + postcode = fields.Char() + date_availability = fields.Date(copy=False, default=lambda self: fields.Date.add(fields.Date.today(), days=3)) + expected_price = fields.Float(required=True) + selling_price = fields.Float(readonly=True, copy=False, default=0.1) + bedrooms = fields.Integer(default=2) + living_area = fields.Integer() + facades = fields.Integer() + garage = fields.Boolean() + garden = fields.Boolean() + garden_area = fields.Integer() + total_area = fields.Integer(compute='_compute_total_area', 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 + garden_orientation = fields.Selection( + string="Garden Orientation", + selection=[ + ('north', 'North'), + ('south', 'South'), + ('east', 'East'), + ('west', 'West')] + ) + @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 = False + + state = fields.Selection( + string="State", + selection=[ + ('new', 'New'), + ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('canceled', 'Canceled')], + default='new', + required=True, + copy=False + ) + active = fields.Boolean(default=False) + property_type_id = fields.Many2one('estate.property.type', string="Property Type") + salesperson_id = fields.Many2one("res.users", string="Salesman", default=lambda self: self.env.user) + buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False) + company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.company) + tag_ids = fields.Many2many('estate.property.tag', string="Tags") + offer_ids = fields.One2many('estate.property.offer', 'property_id', string="Offers") + best_offer = fields.Float(compute="_compute_best_offer", store=True) + + @api.depends("offer_ids.price") + def _compute_best_offer(self): + for record in self: + record.best_offer = max(record.offer_ids.mapped("price"), default=0.0) + + def action_sold(self): + if not self.state == 'canceled': + self.write({ + 'state': 'sold', + }) + return True + else: + raise UserError("The property has been canceled") + + def action_cancel(self): + if not self.state == 'sold': + self.write({ + 'state': 'canceled', + }) + return True + else: + raise UserError("The property has been sold") + + @api.ondelete(at_uninstall=False) + def _unlink_if_correct_states(self): + state_description_values = {elem[0]: elem[1] for elem in self._fields['state']._description_selection(self.env)} + for property in self.filtered(lambda property: property.state not in ['new', 'canceled']): + raise UserError(_('You cannot delete a property which is in %s state.', state_description_values.get(property.state))) + return True + + _sql_constraints = [ + ('check_expected_price', 'CHECK(expected_price > 0)', 'The expected price must be strictly positive'), + ('check_selling_price', 'CHECK(selling_price > 0)', 'The selling price must be strictly positive'), + ] diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..578f0bd73aa --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,107 @@ +from odoo import models, fields, api +from odoo.exceptions import UserError + +class EstatePropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "Real Estate Property Offer" + _order = "price desc" + + price = fields.Float() + status = fields.Selection( + string="Status", + selection=[ + ('accepted', 'Accepted'), + ('refused', 'Refused')], + copy=False, + ) + partner_id = fields.Many2one('res.partner', string="Partner", required=True) + property_id = fields.Many2one('estate.property', string="Property", required=True, ondelete='cascade') + property_type_id = fields.Many2one(related='property_id.property_type_id', string="Property Type") + date_deadline = fields.Date(copy=False, default=fields.Date.today(), compute='_compute_date_deadline', inverse='_inverse_date_deadline', store=True) + validity = fields.Integer(default=7, store=True) + + @api.depends('create_date', 'validity') + def _compute_date_deadline(self): + for record in self: + reference_date = record.create_date or fields.Datetime.now() + record.date_deadline = fields.Date.add(fields.Date.from_string(reference_date), days=record.validity) + + def _inverse_date_deadline(self): + for record in self: + reference_date = record.create_date or fields.Datetime.now() + reference_date = fields.Date.from_string(reference_date) + if record.date_deadline: + record.validity = (record.date_deadline - reference_date).days + + @api.constrains('property_id.expected_price') + def _check_price(self): + for record in self: + if record.price < (record.property_id.expected_price*90/100): + raise UserError("The offer price cannot be lower than 90% of the expected price.") + else: + record.accept_offer() + + def action_accept_offer(self): + self.accept_offer() + return True + + def accept_offer(self): + # Check if there's already an accepted offer + if self.property_id.state == 'offer_accepted': + raise UserError("Another offer has already been accepted.") + + elif self.price < (self.property_id.expected_price*90/100): + raise UserError("The offer price cannot be lower than 90% of the expected price.") + + else: + # Refuse all other offers + self.property_id.offer_ids.filtered(lambda o: o.id != self.id).write({ + 'status': 'refused' + }) + + # Accept this offer and update property + self.write({ + 'status': 'accepted' + }) + self.property_id.write({ + 'selling_price': self.price, + 'buyer_id': self.partner_id.id, + 'state': 'offer_accepted' + }) + return True + + def action_refuse_offer(self): + # Reject this offer and update property + self.write({ + 'status': 'refused' + }) + self.property_id.write({ + 'selling_price': 1.0, + 'buyer_id': False, + 'state': 'new' + }) + return True + + @api.model_create_multi + def create(self, vals_list): + offers = super().create(vals_list) + + for offer in offers: + property = offer.property_id + + # Check if the new offer is higher than existing ones + existing_offers = property.offer_ids.filtered(lambda o: o.id != offer.id) + if existing_offers: + min_existing_price = min(existing_offers.mapped('price')) + if offer.price < min_existing_price: + raise UserError("The new offer price cannot be lower than the existing offer prices.") + + # Set 'offer_received' state only if it's the first offer + if property.state == 'new' and not existing_offers: + property.write({'state': 'offer_received'}) + + return offers + + _sql_constraints = [ + ('check_price', 'CHECK(price > 0)', 'The offer price must be strictly positive.') + ] diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..c8a567e2675 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,13 @@ +from odoo import models, fields + +class EstatePropertyTag(models.Model): + _name = "estate.property.tag" + _description = "Property Tags" + _order = "name" + + name = fields.Char(required=True) + color = fields.Integer(default=1) + + _sql_constraints = [ + ('name_uniq', 'UNIQUE(name)', 'Tag name must be unique') + ] diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..4404c90a9f0 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,20 @@ +from odoo import models, fields, api + +class EstatePropertyType(models.Model): + _name = "estate.property.type" + _description = "Property Types" + _order = "sequence" + + name = fields.Char(required=True) + sequence = fields.Integer() + property_ids = fields.One2many('estate.property', 'property_type_id', string="Properties") + offer_ids = fields.One2many('estate.property.offer', 'property_type_id', string="Offers") + offer_count = fields.Integer(string="Offer Count", compute='_compute_offer_count') + + @api.depends('offer_ids') + def _compute_offer_count(self): + for record in self: + record.offer_count = len(record.offer_ids) + _sql_constraints = [ + ('name_uniq', 'UNIQUE(name)', 'Type name must be unique') + ] diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..8976ff4d138 --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,11 @@ +from odoo import fields, models + +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/report/estate_property_reports.xml b/estate/report/estate_property_reports.xml new file mode 100644 index 00000000000..4e4139a99a0 --- /dev/null +++ b/estate/report/estate_property_reports.xml @@ -0,0 +1,22 @@ + + + + Property Offers + estate.property + qweb-pdf + estate.report_property + estate.report_property + + report + + + + Property Offers Template + res.users + qweb-pdf + estate.report_salesman_properties + estate.report_salesman_properties + + report + + diff --git a/estate/report/estate_property_template.xml b/estate/report/estate_property_template.xml new file mode 100644 index 00000000000..3490ab2ac03 --- /dev/null +++ b/estate/report/estate_property_template.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + diff --git a/estate/security/estate_security.xml b/estate/security/estate_security.xml new file mode 100644 index 00000000000..db1c26b5970 --- /dev/null +++ b/estate/security/estate_security.xml @@ -0,0 +1,33 @@ + + + + Estate Property: Multi-Company Access + + [('company_id', 'in', company_ids)] + + + + + Agent + + + + + Manager + + + + + + Estate Property: Agent Access + + ['|', ('salesperson_id', '=', False), ('salesperson_id', '=', user.id)] + + + + + Estate Property: Manager Access + + + + diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..08232901786 --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,8 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,0 +estate.access_estate_property_type_manager,access_estate_property_type,estate.model_estate_property_type,estate.estate_group_manager,1,1,1,1 +estate.access_estate_property_type_user,access_estate_property_type,estate.model_estate_property_type,estate.estate_group_user,1,0,0,0 +estate.access_estate_property_tag_manager,access_estate_property_tag,estate.model_estate_property_tag,estate.estate_group_manager,1,1,1,1 +estate.access_estate_property_tag_user,access_estate_property_tag,estate.model_estate_property_tag,estate.estate_group_user,1,0,0,0 +estate.access_estate_property_offer,access_estate_property_offer,estate.model_estate_property_offer,base.group_user,1,1,1,1 +estate.access_res_users,access_res_users,base.model_res_users,base.group_user,1,1,1,1 diff --git a/estate/static/description/icon.png b/estate/static/description/icon.png new file mode 100644 index 00000000000..cf7dd4d86ce Binary files /dev/null and b/estate/static/description/icon.png differ diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..440c03fd143 --- /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..6d26f5c9d46 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,42 @@ + + + + estate.property.offer.list + estate.property.offer + + + + + + + +
+

+ + + + + + + + + + + + + + + + + + + Property Types + estate.property.type + list,form + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..5deb26b7cc3 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,140 @@ + + + + estate.property.list + estate.property + + + + + + + + + + + + + + + + + estate.property.form + estate.property + +
+
+
+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + estate.property.search + estate.property + + + + + + + + + + + + + + + + + + + + + + estate.property.kanban + estate.property + + + + +
+
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+
+
+
+
+
+
+ + + Properties + estate.property + list,form,kanban + + {'search_default_available': 1} + +
diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml new file mode 100644 index 00000000000..2aaf7154eaf --- /dev/null +++ b/estate/views/res_users_views.xml @@ -0,0 +1,20 @@ + + + + + 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..d13dee2389a --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +{ + 'name': "Real Estate Account", + 'summary': """ + Real Estate Account Management + """, + 'description': """ + Real Estate Account Management App + """, + 'author': "Rahul", + 'website': "https://www.odoo.com/", + 'category': 'Tutorials/RealEstateAccount', + 'version': '0.1', + 'application': True, + 'installable': True, + 'depends': ['estate', 'account'], + 'data': [ + 'report/estate_account_reports.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..719ae78a55b --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,40 @@ +from odoo import models, fields, api, Command +from odoo.exceptions import UserError + +class EstateProperty(models.Model): + _inherit = "estate.property" + + def action_sold(self): + + try: + self.check_access('write') + except AccessError: + raise UserError("You don't have the right to edit this property in its current state.") + + self.env['account.move'].sudo().create( + { + "name": "Invoice from Property %s" % (self.name), + "move_type": 'out_invoice', + "partner_id": self.buyer_id.id, + "invoice_date": fields.Date.today(), + "line_ids": [ + Command.create({ + 'name': 'Congrats on your new property', + 'quantity': 1, + 'price_unit': self.selling_price, + }), + Command.create({ + 'name': 'Taxes', + 'quantity': 1, + 'price_unit': self.selling_price*0.06, + }), + Command.create({ + 'name': 'Administrative Fees', + 'quantity': 1, + 'price_unit': 100, + }) + ], + } + ) + + return super().action_sold() diff --git a/estate_account/report/estate_account_reports.xml b/estate_account/report/estate_account_reports.xml new file mode 100644 index 00000000000..ed9d507f755 --- /dev/null +++ b/estate_account/report/estate_account_reports.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/mo_report/__init__.py b/mo_report/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/mo_report/__manifest__.py b/mo_report/__manifest__.py new file mode 100644 index 00000000000..d17cbfd89c3 --- /dev/null +++ b/mo_report/__manifest__.py @@ -0,0 +1,13 @@ +{ + 'name': 'MO Report (w/out Kit Products)', + 'version': '18.0', + 'description': 'MO Delivery Note Report without Kit Products', + 'depends': ['sale_management', 'mrp'], + 'author': 'Rahul Jha (jhra)', + 'website': 'https://www.odoo.com', + 'category': 'Manufacturing/MO Report', + 'data': [ + 'report/report_manufacturing_order_template.xml' + ], + 'license': 'LGPL-3', +} \ No newline at end of file diff --git a/mo_report/report/report_manufacturing_order_template.xml b/mo_report/report/report_manufacturing_order_template.xml new file mode 100644 index 00000000000..f8f0be27b61 --- /dev/null +++ b/mo_report/report/report_manufacturing_order_template.xml @@ -0,0 +1,25 @@ + + + + + + + MO Delivery Note + stock.picking + qweb-pdf + mo_report.report_manufacturing_order + mo_report.report_manufacturing_order + 'MO Delivery Note -- %s - %s' % (object.partner_id.name or '', object.name) + + report + + + \ No newline at end of file diff --git a/supplier_portal/__init__.py b/supplier_portal/__init__.py new file mode 100644 index 00000000000..a03bfd097a6 --- /dev/null +++ b/supplier_portal/__init__.py @@ -0,0 +1 @@ +from . import controllers \ No newline at end of file diff --git a/supplier_portal/__manifest__.py b/supplier_portal/__manifest__.py new file mode 100644 index 00000000000..334b05c6c81 --- /dev/null +++ b/supplier_portal/__manifest__.py @@ -0,0 +1,13 @@ +{ + 'name': 'Supplier Portal', + 'version': '18.0', + 'description': 'Supplier Portal', + 'depends': ['portal','sale_management'], + 'author': 'Rahul Jha (jhra)', + 'website': 'https://www.odoo.com', + 'category': 'Supplier/Portal', + 'data': [ + 'views/portal_templates.xml' + ], + 'license': 'LGPL-3', +} diff --git a/supplier_portal/controllers/__init__.py b/supplier_portal/controllers/__init__.py new file mode 100644 index 00000000000..8c3feb6f562 --- /dev/null +++ b/supplier_portal/controllers/__init__.py @@ -0,0 +1 @@ +from . import portal diff --git a/supplier_portal/controllers/portal.py b/supplier_portal/controllers/portal.py new file mode 100644 index 00000000000..a240b723688 --- /dev/null +++ b/supplier_portal/controllers/portal.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +from datetime import date +import datetime +from odoo import http +from odoo.http import request, route +from odoo.exceptions import AccessError +import base64 + +class SupplierPortal(http.Controller): + + @route(['/my/supplier_portal'], type='http', auth="user", website=True) + def supplier_portal(self, **kw): + return request.render("supplier_portal.supplier_portal_login_success", {}) + + @route(['/my/supplier_portal/upload_invoice'], type='http', auth="user", methods=['POST'], website=True) + def upload_invoice(self, **post): + supplier = request.env.user.partner_id + organisation_id = int(post.get('organisation_id')) + + # Get uploaded files + pdf_file = post.get('pdf_file') + xml_file = post.get('xml_file') + + # Read and encode files + pdf_content = pdf_file.read() + xml_content = xml_file.read() + + today = date.today() + + # Create vendor bill (Draft state) + bill_vals = { + 'partner_id': supplier.id, + 'company_id': organisation_id, + 'invoice_date': today, + 'invoice_date_due': today+datetime.timedelta(days=7), + 'state': 'draft', + 'move_type': 'in_invoice', # Vendor Bill + 'invoice_line_ids': [], + } + bill = request.env['account.move'].sudo().create(bill_vals) + + # Attach files + request.env['ir.attachment'].sudo().create({ + 'name': pdf_file.filename, + 'type': 'binary', + 'datas': base64.b64encode(pdf_content), + 'res_model': 'account.move', + 'res_id': bill.id, + }) + + request.env['ir.attachment'].sudo().create({ + 'name': xml_file.filename, + 'type': 'binary', + 'datas': base64.b64encode(xml_content), + 'res_model': 'account.move', + 'res_id': bill.id, + }) + + return request.render('supplier_portal.supplier_portal_upload_success',{}) diff --git a/supplier_portal/static/src/img/bag.svg b/supplier_portal/static/src/img/bag.svg new file mode 100644 index 00000000000..148d08a2266 --- /dev/null +++ b/supplier_portal/static/src/img/bag.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/supplier_portal/views/portal_templates.xml b/supplier_portal/views/portal_templates.xml new file mode 100644 index 00000000000..6663aa7c621 --- /dev/null +++ b/supplier_portal/views/portal_templates.xml @@ -0,0 +1,61 @@ + + + + + + + + + diff --git a/update_discount/__init__.py b/update_discount/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/update_discount/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/update_discount/__manifest__.py b/update_discount/__manifest__.py new file mode 100644 index 00000000000..fcb5815482c --- /dev/null +++ b/update_discount/__manifest__.py @@ -0,0 +1,10 @@ +{ + 'name': 'Discount Update', + 'version': '18.0', + 'description': 'Update Discount', + 'depends': ['sale_management'], + 'author': 'Rahul Jha (jhra)', + 'website': 'https://www.odoo.com', + 'category': 'Sales/Discount Update', + 'license': 'LGPL-3', +} diff --git a/update_discount/models/__init__.py b/update_discount/models/__init__.py new file mode 100644 index 00000000000..6aacb753131 --- /dev/null +++ b/update_discount/models/__init__.py @@ -0,0 +1 @@ +from . import sale_order diff --git a/update_discount/models/sale_order.py b/update_discount/models/sale_order.py new file mode 100644 index 00000000000..d54d1cb45e6 --- /dev/null +++ b/update_discount/models/sale_order.py @@ -0,0 +1,189 @@ +from odoo import models, fields, api, Command +from collections import defaultdict + +class SaleOrder(models.Model): + _inherit = 'sale.order' + + has_global_discount = fields.Boolean(string="Has Global Discount", compute='_compute_has_global_discount', store=True) + global_discount_percentage = fields.Float(string="Global Discount (%)", store=True) + + @api.depends('order_line', 'order_line.product_id', 'company_id.sale_discount_product_id') + def _compute_has_global_discount(self): + for order in self: + discount_product = order.company_id.sale_discount_product_id + if not discount_product: + order.has_global_discount = False + continue + + order.has_global_discount = bool(order.order_line.filtered( + lambda l: l.product_id.id == discount_product.id)) + + @api.model_create_multi + def create(self, vals_list): + orders = super().create(vals_list) + # Initialize global discount for new orders + for order in orders: + if order.has_global_discount and order.global_discount_percentage: + order._update_global_discount() + return orders + + def write(self, vals): + result = super().write(vals) + # If order lines were modified, update discount + if 'order_line' in vals: + for order in self: + if order.has_global_discount and order.global_discount_percentage: + order._update_global_discount() + return result + + def _get_discount_lines(self): + """Return order lines that are discount lines""" + self.ensure_one() + discount_product = self.company_id.sale_discount_product_id + if not discount_product: + return self.env['sale.order.line'] + + return self.order_line.filtered( + lambda l: l.product_id.id == discount_product.id) + + def _get_regular_lines(self): + """Return order lines that are not discount lines""" + self.ensure_one() + discount_product = self.company_id.sale_discount_product_id + if not discount_product: + return self.order_line + + return self.order_line.filtered( + lambda l: l.product_id.id != discount_product.id) + + def _update_global_discount(self): + """Update discount lines based on current order lines""" + self.ensure_one() + + # Check if we have regular product lines + regular_lines = self._get_regular_lines() + discount_lines = self._get_discount_lines() + + if not regular_lines: + # Remove discount lines if no regular lines exist + if discount_lines: + discount_lines.unlink() + self.global_discount_percentage = 0 + return + + # Remove existing discount lines + if discount_lines: + discount_lines.unlink() + + # Create new discount lines based on current order lines + self._create_global_discount_lines(self.global_discount_percentage / 100) + + def _create_global_discount_lines(self, discount_percentage): + """Create discount lines based on current order lines and discount percentage""" + self.ensure_one() + discount_product = self.company_id.sale_discount_product_id + if not discount_product: + return False + + # Group by tax combinations + total_price_per_tax_groups = defaultdict(float) + for line in self._get_regular_lines(): + if not line.product_uom_qty or not line.price_unit: + continue + total_price_per_tax_groups[line.tax_id] += (line.price_unit * line.product_uom_qty) + + if not total_price_per_tax_groups: + return False + + vals_list = [] + if len(total_price_per_tax_groups) == 1: + # No taxes, or all lines have the exact same taxes + taxes = next(iter(total_price_per_tax_groups.keys())) + subtotal = total_price_per_tax_groups[taxes] + vals = { + 'order_id': self.id, + 'product_id': discount_product.id, + 'sequence': 999, + 'price_unit': -subtotal * discount_percentage, + 'tax_id': [Command.set(taxes.ids)], + 'name': f"Discount: {discount_percentage*100}%", + 'product_uom_qty': 1.0, + 'product_uom': discount_product.uom_id.id, + } + vals_list.append(vals) + else: + # Multiple tax groups + for taxes, subtotal in total_price_per_tax_groups.items(): + vals = { + 'order_id': self.id, + 'product_id': discount_product.id, + 'sequence': 999, + 'price_unit': -subtotal * discount_percentage, + 'tax_id': [Command.set(taxes.ids)], + 'name': f"Discount: {discount_percentage*100}% - On products with taxes: {', '.join(taxes.mapped('name'))}", + 'product_uom_qty': 1.0, + 'product_uom': discount_product.uom_id.id, + } + vals_list.append(vals) + + if not vals_list: + return False + + lines = self.env['sale.order.line'].create(vals_list) + return lines + +class SaleOrderLine(models.Model): + _inherit = 'sale.order.line' + + @api.model_create_multi + def create(self, vals_list): + lines = super().create(vals_list) + # Avoid triggering for discount product lines to prevent infinite loop + self._trigger_discount_update(lines) + return lines + + def write(self, vals): + result = super().write(vals) + self._trigger_discount_update(self) + return result + + def unlink(self): + orders = self.mapped('order_id') + result = super().unlink() + # After unlinking, update discount on affected orders + for order in orders: + if order.exists() and order.has_global_discount and order.global_discount_percentage: + order._update_global_discount() + return result + + def _trigger_discount_update(self, lines): + """Trigger discount update for affected orders, avoiding recursion""" + # Group by order + orders_to_update = self.env['sale.order'] + for line in lines: + # Skip discount product lines to avoid infinite recursion + if line.product_id and line.order_id and line.order_id.company_id.sale_discount_product_id: + if line.product_id.id != line.order_id.company_id.sale_discount_product_id.id: + if line.order_id.has_global_discount and line.order_id.global_discount_percentage: + orders_to_update |= line.order_id + + # Update discounts for affected orders + for order in orders_to_update: + order._update_global_discount() + + +class SaleOrderDiscount(models.TransientModel): + _inherit = 'sale.order.discount' + + def action_apply_discount(self): + self.ensure_one() + self = self.with_company(self.company_id) + print("Self:", self) + + if self.discount_type == 'so_discount': + self.sale_order_id.global_discount_percentage = self.discount_percentage * 100 + result = super().action_apply_discount() + self.sale_order_id._update_global_discount() + return result + else: + return super().action_apply_discount() diff --git a/warranty_extension/__init__.py b/warranty_extension/__init__.py new file mode 100644 index 00000000000..9b4296142f4 --- /dev/null +++ b/warranty_extension/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard diff --git a/warranty_extension/__manifest__.py b/warranty_extension/__manifest__.py new file mode 100644 index 00000000000..0e3e96c5e1e --- /dev/null +++ b/warranty_extension/__manifest__.py @@ -0,0 +1,22 @@ +{ + 'name': "Warranty Extension", + 'version': '18.0', + 'depends': ['sale_management','stock'], + 'author': "Rahul", + 'website': "https://www.odoo.com/", + 'category': 'Sales/Warranty Extension', + 'summary': 'Warranty Extension', + 'description': """ + Inheritance Demonstration + """, + 'data': [ + 'security/ir.model.access.csv', + 'wizard/warranty_selection_wizard.xml', + 'views/warranty_configuration_views.xml', + 'views/product_template_views.xml', + 'views/sale_order_views.xml', + ], + 'installable': True, + 'application': True, + 'license': 'LGPL-3' +} diff --git a/warranty_extension/models/__init__.py b/warranty_extension/models/__init__.py new file mode 100644 index 00000000000..75e9f0c627e --- /dev/null +++ b/warranty_extension/models/__init__.py @@ -0,0 +1,4 @@ +from . import warranty_configuration +from . import product_template +from . import sale_order +from . import warranty_lines diff --git a/warranty_extension/models/product_template.py b/warranty_extension/models/product_template.py new file mode 100644 index 00000000000..64111727787 --- /dev/null +++ b/warranty_extension/models/product_template.py @@ -0,0 +1,8 @@ +from odoo import models, fields + + +class ProductTemplate(models.Model): + _inherit = 'product.template' + + warranty_configuration_ids = fields.Many2many('warranty.configuration', string="Warranty Configurations") + is_warranty = fields.Boolean(string='Is Warranty') diff --git a/warranty_extension/models/sale_order.py b/warranty_extension/models/sale_order.py new file mode 100644 index 00000000000..305c72063d5 --- /dev/null +++ b/warranty_extension/models/sale_order.py @@ -0,0 +1,83 @@ +from odoo import models, fields, api + +class SaleOrder(models.Model): + _inherit = "sale.order" + + def action_warranty_selection_wizard(self): + self.ensure_one() + action = self.env["ir.actions.act_window"]._for_xml_id("warranty_extension.action_warranty_selection_wizard") + warranty_line_ids = [] + + # Only process products that have warranties available + for line in self.order_line.filtered(lambda l: l.product_id.product_tmpl_id.warranty_configuration_ids): + product_tmpl = line.product_id.product_tmpl_id + print(f"Processing product {product_tmpl.name} with {len(product_tmpl.warranty_configuration_ids)} warranties") + + for warranty in product_tmpl.warranty_configuration_ids: + print(f"Adding warranty option: {warranty.name} for product {product_tmpl.name}") + warranty_line_ids.append((0, 0, { + 'product_id': product_tmpl.id, + 'warranty_id': warranty.id, + 'end_date': fields.Date.add(fields.Date.today(), years=warranty.year), + 'selected': False + })) + + if not warranty_line_ids: + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': 'No Warranties Available', + 'message': 'None of the products in this order have warranties available.', + 'type': 'warning', + 'sticky': False, + } + } + + action['context'] = { + 'default_sale_order_id': self.id, + 'default_warranty_lines': warranty_line_ids, + } + + return action + + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + is_warranty = fields.Boolean(string="Is Warranty", default=False) + + @api.model_create_multi + def create(self, vals_list): + """Ensure warranty lines are marked accordingly on creation.""" + for vals in vals_list: + if vals.get("is_warranty") and "product_id" in vals: + product = self.env["product.template"].browse(vals["product_id"]) + if product: + vals["name"] = f"Warranty for {product.name}" + return super().create(vals_list) + + def unlink(self): + """Remove associated warranty lines when the main product is deleted.""" + lines_to_delete = self.env['sale.order.line'] + + for line in self: + # If this is a warranty line (has 'Valid until' in name), just add it + if "Valid until" in (line.name or ''): + lines_to_delete |= line + continue + + # For product lines, find and delete their warranty lines + warranty_lines = line.order_id.order_line.filtered( + lambda l: f"For product: {line.product_id.name}" in (l.name or '') + and f"Valid until" in (l.name or '') + ) + if warranty_lines: + print(f"Found {len(warranty_lines)} warranty lines for product {line.product_id.name}") + lines_to_delete |= warranty_lines + + # Add the product line itself + lines_to_delete |= line + + return super(SaleOrderLine, lines_to_delete).unlink() diff --git a/warranty_extension/models/warranty_configuration.py b/warranty_extension/models/warranty_configuration.py new file mode 100644 index 00000000000..ba2d8fb2ea7 --- /dev/null +++ b/warranty_extension/models/warranty_configuration.py @@ -0,0 +1,16 @@ +from odoo import models, fields, api + + +class WarrantyConfiguration(models.Model): + _name = 'warranty.configuration' + _description = 'Warranty Configuration' + + name = fields.Char(required=True, compute='_compute_name') + product_tmpl_id = fields.Many2one('product.template', string="Product Template", required=True) + year = fields.Integer(string="Year", required=True) + percentage = fields.Float(string='Percentage', required=True) + + @api.depends('year') + def _compute_name(self): + for record in self: + record.name = f'{record.year} Year' diff --git a/warranty_extension/models/warranty_lines.py b/warranty_extension/models/warranty_lines.py new file mode 100644 index 00000000000..eabfc0e9772 --- /dev/null +++ b/warranty_extension/models/warranty_lines.py @@ -0,0 +1,11 @@ +from odoo import models, fields + +class WarrantySelectionWizardLine(models.TransientModel): + _name = 'warranty.selection.wizard.line' + _description = 'Warranty Selection Wizard Line' + + wizard_id = fields.Many2one('warranty.selection.wizard', string="Warranty Selection Wizard") + product_id = fields.Many2one('product.template', string="Product") + warranty_id = fields.Many2one('warranty.configuration', string="Warranty") + end_date = fields.Date(string="End Date") + selected = fields.Boolean(string="Select", default=False) diff --git a/warranty_extension/security/ir.model.access.csv b/warranty_extension/security/ir.model.access.csv new file mode 100644 index 00000000000..1446c222cd0 --- /dev/null +++ b/warranty_extension/security/ir.model.access.csv @@ -0,0 +1,5 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_product_template,access_product_template,model_product_template,base.group_user,1,1,1,1 +access_warranty_configuration,access_warranty_configuration,model_warranty_configuration,base.group_user,1,1,1,1 +access_warranty_selection_wizard,access_warranty_selection_wizard,model_warranty_selection_wizard,base.group_user,1,1,1,1 +access_warranty_selection_wizard_line,access_warranty_selection_wizard_line,model_warranty_selection_wizard_line,base.group_user,1,1,1,1 diff --git a/warranty_extension/static/description/icon.png b/warranty_extension/static/description/icon.png new file mode 100644 index 00000000000..c38cd26a6b7 Binary files /dev/null and b/warranty_extension/static/description/icon.png differ diff --git a/warranty_extension/views/product_template_views.xml b/warranty_extension/views/product_template_views.xml new file mode 100644 index 00000000000..aea6600ab94 --- /dev/null +++ b/warranty_extension/views/product_template_views.xml @@ -0,0 +1,14 @@ + + + + warranty.extension.product.template.view.form + product.template + + + + + + + + + diff --git a/warranty_extension/views/quotation_warranty_views.xml b/warranty_extension/views/quotation_warranty_views.xml new file mode 100644 index 00000000000..d30c1697b17 --- /dev/null +++ b/warranty_extension/views/quotation_warranty_views.xml @@ -0,0 +1,13 @@ + + + + warranty.extension.quotation.view.form + sale.order + + + + + + diff --git a/warranty_extension/views/sale_order_views.xml b/warranty_extension/views/sale_order_views.xml new file mode 100644 index 00000000000..5fc0b526122 --- /dev/null +++ b/warranty_extension/views/sale_order_views.xml @@ -0,0 +1,18 @@ + + + + sale.order.form.inherit.warranty + sale.order + + + +