diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py index 31406e8addb..57ddff7e3ec 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/lazy_load_wrapper.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..ce4a1b1a540 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -0,0 +1,47 @@ +/** @odoo-module **/ + +import { Component, useState } from "@odoo/owl"; +import { useService } from "@web/core/utils/hooks"; +import { Layout } from "@web/search/layout"; +import { DashboardItem } from "./dashboard_item"; +import { Piechart } from "./piechart"; +import { registry } from "@web/core/registry"; +import { DBModal } from "./dashboard_setting_modal"; + +export class AwesomeDashboard extends Component { + static template = "awesome_dashboard.AwesomeDashboard"; + + static components = { Layout, DashboardItem, Piechart }; + + setup() { + this.action = useService("action"); + this.statisticService = useService("load_statistics"); + this.data = useState(this.statisticService); + this.dialog = useService("dialog"); + } + + openMyModal() { + this.dialog.add(DBModal, { + items: this.data.stats, + chart: this.data.chartData, + }); + } + + viewCustomers() { + this.action.doAction("base.action_partner_form"); + } + + viewLeads() { + this.action.doAction({ + type: "ir.actions.act_window", + target: "current", + res_model: "crm.lead", + views: [ + [false, "form"], + [false, "list"], + ], + }); + } +} + +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..aa0e28e0630 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.scss @@ -0,0 +1,6 @@ +.o_dashboard { + background-color: #111827; + .db-item-title { + font-size:18px; + } +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml new file mode 100644 index 00000000000..acc5ca9cfda --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -0,0 +1,26 @@ + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+ +
diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item.js b/awesome_dashboard/static/src/dashboard/dashboard_item.js new file mode 100644 index 00000000000..21b3ff2ae6a --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item.js @@ -0,0 +1,15 @@ +/** @odoo-module **/ + +import { Component } from "@odoo/owl"; + +export class DashboardItem extends Component { + static template = "awesome_dashboard.dashboard_item"; + + static components = {}; + + static props = ["size", "title", "value"]; + + static defaultProps = { + size: 1, + }; +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item.xml b/awesome_dashboard/static/src/dashboard/dashboard_item.xml new file mode 100644 index 00000000000..08dfb5af9df --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item.xml @@ -0,0 +1,11 @@ + + + + +
+

+

+

+
+ +
diff --git a/awesome_dashboard/static/src/dashboard/dashboard_setting_modal.js b/awesome_dashboard/static/src/dashboard/dashboard_setting_modal.js new file mode 100644 index 00000000000..f0075032006 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_setting_modal.js @@ -0,0 +1,46 @@ +import { Component } from "@odoo/owl"; +import { Dialog } from "@web/core/dialog/dialog"; + +export class DBModal extends Component { + static template = "awesome_dashboard.db_modal"; + static components = { Dialog }; + + static props = ['items','chart'] + + setup() { + this.items = this.props.items; + this.chart = this.props.chart; + this.visibleList = this.items.reduce((acc, crr) => { + if (crr?.isVisible) { + acc?.push(crr?.id); + } + return acc; + }, []); + + if (this.chart.isVisible) { + this.visibleList.push("chart"); + } + } + + handleItemToggle = (_, id) => { + if (this.visibleList.includes(id)) { + this.visibleList = this.visibleList.filter((i) => i !== id); + } else { + this.visibleList.push(id); + } + }; + + handleApplySetting() { + this.items.forEach((item) => { + item.isVisible = this.visibleList.includes(item?.id); + }); + + this.chart.isVisible = this.visibleList.includes("chart"); + + localStorage.setItem( + "dashboardItemVisibility", + JSON.stringify(this.visibleList) + ); + this.props.close(); + } +} diff --git a/awesome_dashboard/static/src/dashboard/dashboard_setting_modal.xml b/awesome_dashboard/static/src/dashboard/dashboard_setting_modal.xml new file mode 100644 index 00000000000..8f3443e764d --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_setting_modal.xml @@ -0,0 +1,21 @@ + + + + +

Which cards do you wish to see ?

+ +
+ +

+

+
+
+ +

T-shirts Sales by size

+
+ + + +
+
+
diff --git a/awesome_dashboard/static/src/dashboard/piechart.js b/awesome_dashboard/static/src/dashboard/piechart.js new file mode 100644 index 00000000000..5742f03d2fc --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/piechart.js @@ -0,0 +1,59 @@ +import { Component, onWillStart, useRef, useEffect } from "@odoo/owl"; +import { loadJS } from "@web/core/assets"; + +export class Piechart extends Component { + static template = "awesome_dashboard.piechart"; + + static components = {}; + + static props = ["chartData"]; + + setup() { + this.canvasRef = useRef("canvas"); + this.chart = null; + this.chartData = this.props.chartData; + onWillStart(() => loadJS(["/web/static/lib/Chart/Chart.js"])); + useEffect( + () => this.renderChart(), + () => [this.chartData] + ); + } + + renderChart() { + if (this.chart) { + this.chart.destroy(); + } + this.chart = new Chart(this.canvasRef.el, { + data: this.chartData, + type: "pie", + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: "bottom", + labels: { + padding: 20, + usePointStyle: true, + pointStyle: "rect", + font: { + size: 16, + weight: "bold", + color: "#fff", + }, + }, + }, + title: { + display: true, + text: "T-Shirt Sales by Size", + font: { + size: 16, + weight: "bold", + }, + padding: 0, + }, + }, + }, + }); + } +} diff --git a/awesome_dashboard/static/src/dashboard/piechart.xml b/awesome_dashboard/static/src/dashboard/piechart.xml new file mode 100644 index 00000000000..3abcc167214 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/piechart.xml @@ -0,0 +1,10 @@ + + + + +
+ +
+
+ +
diff --git a/awesome_dashboard/static/src/dashboard/statistic.js b/awesome_dashboard/static/src/dashboard/statistic.js new file mode 100644 index 00000000000..3d9af6f8159 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/statistic.js @@ -0,0 +1,62 @@ +import { registry } from "@web/core/registry"; +import { rpc } from "@web/core/network/rpc"; +import { reactive } from "@odoo/owl"; + +const statsMap = { + average_quantity: { id: 1, title: "Average quanitity order" }, + average_time: { id: 2, title: "Average time for order from new to sent" }, + nb_cancelled_orders: { id: 3, title: "Number of cancelled order this month" }, + nb_new_orders: { id: 4, title: "Number of new orders this month" }, + total_amount: { id: 5, title: "Total amount of new orders" }, +}; + +const statisticService = { + start() { + let stats = reactive([]); + let chartData = reactive({}); + const loadStatistics = async () => { + const result = await rpc("/awesome_dashboard/statistics"); + const dbItemVisibility = localStorage.getItem("dashboardItemVisibility"); + let formatedres = Object.entries(result).reduce((prev, [key, value]) => { + const item = statsMap[key]; + + if (item) { + prev.push({ + id: item?.id, + title: item?.title, + size: item?.title?.length > 30 ? 2 : 1, + value, + isVisible: dbItemVisibility + ? dbItemVisibility.includes(item?.id) + : true, + }); + } else if (typeof value === "object") { + chartData.labels = Object.keys(value); + chartData.datasets = [ + { + label: "Order by size", + data: Object.values(value), + }, + ]; + chartData.isVisible = dbItemVisibility + ? dbItemVisibility.includes("chart") + : true; + } + return prev; + }, []); + + stats?.push(...formatedres); + + return { stats, chartData }; + }; + + loadStatistics(); + + return { + stats, + chartData, + }; + }, +}; + +registry.category("services").add("load_statistics", statisticService); diff --git a/awesome_dashboard/static/src/lazy_load_wrapper.js b/awesome_dashboard/static/src/lazy_load_wrapper.js new file mode 100644 index 00000000000..3e5573987d4 --- /dev/null +++ b/awesome_dashboard/static/src/lazy_load_wrapper.js @@ -0,0 +1,16 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; +import { LazyComponent } from "@web/core/assets"; +import { xml, Component } from "@odoo/owl"; + +class DashoboardLazyLoader extends Component { + static components = { LazyComponent }; + static template = xml` + + `; +} + +registry + .category("actions") + .add("awesome_dashboard.dashboard", DashoboardLazyLoader); diff --git a/awesome_owl/static/src/card.js b/awesome_owl/static/src/card.js new file mode 100644 index 00000000000..c4b95c1754a --- /dev/null +++ b/awesome_owl/static/src/card.js @@ -0,0 +1,15 @@ +import { Component , useState } from "@odoo/owl"; + +export class Card extends Component { + static template = "awesome_owl.card"; + + static components = { } + + setup(){ + this.showCardContent = useState({value:true}); + } + + toggleCardContent(){ + this.showCardContent.value = !this.showCardContent.value; + } +} diff --git a/awesome_owl/static/src/card.xml b/awesome_owl/static/src/card.xml new file mode 100644 index 00000000000..043bb85afea --- /dev/null +++ b/awesome_owl/static/src/card.xml @@ -0,0 +1,20 @@ + + + + +
+
+

+ +

+ +
+ +
+ +
+ +
+
+ +
diff --git a/awesome_owl/static/src/counter.js b/awesome_owl/static/src/counter.js new file mode 100644 index 00000000000..1055e8ebf1a --- /dev/null +++ b/awesome_owl/static/src/counter.js @@ -0,0 +1,18 @@ +/** @odoo-module **/ + +import { Component , useState } from "@odoo/owl"; + +export class Counter extends Component { + static template = "awesome_owl.counter"; + + static props = ['btnIndex','onchange'] + + setup(){ + this.count = useState({value:0}); + } + + do_maths(){ + this.count.value++; + this.props.onchange(); + } +} diff --git a/awesome_owl/static/src/counter.xml b/awesome_owl/static/src/counter.xml new file mode 100644 index 00000000000..e9024045ba5 --- /dev/null +++ b/awesome_owl/static/src/counter.xml @@ -0,0 +1,11 @@ + + + +
+

Counter:

+ +
+
+
diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 657fb8b07bb..3f0f5dc5918 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,7 +1,20 @@ /** @odoo-module **/ -import { Component } from "@odoo/owl"; +import { Component , useState } from "@odoo/owl"; +import { Counter } from "./counter"; +import { TodoList } from "./todo/todo_list"; +import { Card } from "./card"; export class Playground extends Component { static template = "awesome_owl.playground"; + + static components = { Counter ,TodoList , Card} + + setup(){ + this.sum = useState({value:0}) + } + + increment(){ + this.sum.value++; + } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..aaa17082a67 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -1,9 +1,27 @@ - +
- hello world +

Sum is

+ + + + Counters + + + + + + + + + Todo list + + + + +
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..7592128e9ed --- /dev/null +++ b/awesome_owl/static/src/todo/todo_item.js @@ -0,0 +1,8 @@ +import { Component , useState } from "@odoo/owl"; + +export class TodoItem extends Component { + static template = "awesome_owl.todo_item"; + + static props = ['todo','toggleState','removeItem'] + +} 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..4d1004697f6 --- /dev/null +++ b/awesome_owl/static/src/todo/todo_item.xml @@ -0,0 +1,11 @@ + + + +
+ + +

+ +

+
+
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..bfb78cd2850 --- /dev/null +++ b/awesome_owl/static/src/todo/todo_list.js @@ -0,0 +1,53 @@ +import { Component , useState , useRef , onMounted } from "@odoo/owl"; +import { TodoItem } from "./todo_item"; + +function useAutoFocus(refName){ + let inputRef = useRef(refName); + onMounted(()=>{ + inputRef.el.focus() + }) +} + +export class TodoList extends Component { + static template = "awesome_owl.todo_list"; + + static components = { TodoItem } + + setup(){ + this.todos = useState([]); + this.idCount = 1; + + useAutoFocus('todo_input'); + } + + addTodo(e){ + const value = e.target.value; + if(value){ + if(e.key === "Enter"){ + + this.todos.unshift({ + id:this.idCount++, + description:value, + isCompleted:false + }); + + e.target.value = "" + } + } + + } + + removeItem(id){ + const itmIndex = this.todos.findIndex( itm => itm.id == id); + if(itmIndex+1){ + this.todos.splice(itmIndex,1); + } + } + + toggleState(id){ + const item = this.todos.find( x => x.id == id); + if(item){ + item.isCompleted = !item.isCompleted; + } + } +} 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..2c3daee65b8 --- /dev/null +++ b/awesome_owl/static/src/todo/todo_list.xml @@ -0,0 +1,12 @@ + + + +
+

Todo items

+ + + + +
+
+
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..9f54891ea5b --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,22 @@ +{ + "name": "Real Estate", + "depends": ["base"], + "license": "LGPL-3", + "data": [ + "security/security.xml", + "security/ir.model.access.csv", + "security/estate_property_rules.xml", + "views/estate_property_views.xml", + "views/estate_menus_views.xml", + "views/estate_offers_views.xml", + "views/estate_property_type_views.xml", + "views/res_users_views.xml", + "data/estate_property_type_data.xml", + "data/estate_property_data.xml", + "data/estate_property_offer_data.xml", + "report/estate_property_offers_template.xml", + "report/estate_property_reports.xml", + ], + "category": "Real Estate/Brokerage", + "application": True, +} diff --git a/estate/data/estate_property_data.xml b/estate/data/estate_property_data.xml new file mode 100644 index 00000000000..ff6bf2befb8 --- /dev/null +++ b/estate/data/estate_property_data.xml @@ -0,0 +1,77 @@ + + + + + + Big Villa + new + A nice and big villa + 12345 + 2020-02-02 + + + + + + + + + + south + + + + Trailer home + cancelled + Home in trailer park + 54321 + 2001-01-01 + + + + + + + + + + + + + + Cozy hut + new + Home in desert + 54321 + 2001-01-01 + + + + + + + + + + + + + + diff --git a/estate/data/estate_property_offer_data.xml b/estate/data/estate_property_offer_data.xml new file mode 100644 index 00000000000..9d983752c3a --- /dev/null +++ b/estate/data/estate_property_offer_data.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/estate/data/estate_property_type_data.xml b/estate/data/estate_property_type_data.xml new file mode 100644 index 00000000000..c9e091f77e3 --- /dev/null +++ b/estate/data/estate_property_type_data.xml @@ -0,0 +1,22 @@ + + + + + + Residential + + + + Commercial + + + + Industrial + + + + Land + + + + diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..089edd5ffd0 --- /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 inherited_estate diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..0255ff8b20f --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,130 @@ +from odoo import api, fields, models +from dateutil.relativedelta import relativedelta +from odoo.exceptions import UserError, ValidationError + + +class Property(models.Model): + _name = "estate.property" + _description = "Estate Properties" + _sql_constraints = [ + ( + "check_expected_price", + "CHECK(expected_price > 0)", + "The expected price of a property must be positive.", + ), + ( + "check_selling_price", + "CHECK(selling_price >= 0)", + "The selling price of a property must be positive.", + ), + ] + _order = "id desc" + + name = fields.Char(required=True) + description = fields.Text() + postcode = fields.Char() + date_availability = fields.Date( + default=fields.Date.today() + relativedelta(months=3), copy=False + ) + expected_price = fields.Float(required=True) + selling_price = fields.Float(readonly=True, copy=False) + bedrooms = fields.Integer(default=2) + living_area = fields.Integer() + facades = fields.Integer() + garage = fields.Boolean() + garden = fields.Boolean() + garden_area = fields.Integer() + active = fields.Boolean(default=True) + property_type_id = fields.Many2one("estate.property.type", string="Property Type") + salesman = fields.Many2one("res.users", default=lambda self: self.env.user) + company_id = fields.Many2one( + "res.company", + string="Company", + required=True, + default=lambda self: self.env.company, + ) + buyer = fields.Many2one("res.partner", copy=False) + tag_ids = fields.Many2many("estate.property.tag") + offer_ids = fields.One2many("estate.property.offer", "property_id") + state = fields.Selection( + string="Status", + selection=[ + ("new", "New"), + ("offer_received", "Offer received"), + ("offer_accepted", "Offer accepted"), + ("sold", "Sold"), + ("cancelled", "Cancelled"), + ], + required=True, + copy=False, + default="new", + ) + garden_orientation = fields.Selection( + selection=[ + ("north", "North"), + ("south", "South"), + ("east", "East"), + ("west", "West"), + ], + help="This is Garden orientation described in directions", + ) + total_area = fields.Integer(compute="_compute_total_area") + best_price = fields.Float(string="Best offer", compute="_compute_best_price") + + @api.depends("garden_area", "living_area") + def _compute_total_area(self): + for prp in self: + prp.total_area = prp.garden_area + prp.living_area + + @api.depends("offer_ids.price") + def _compute_best_price(self): + for prp in self: + pricelist = prp.mapped("offer_ids.price") + if len(pricelist) > 0: + prp.best_price = max(pricelist) + else: + prp.best_price = 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 set_sold_state(self): + if self.state != "cancelled": + if self.state == "offer_accepted": + self.state = "sold" + else: + raise UserError("Only Accepeted offers property can be sold") + else: + raise UserError("Cancelled property can not be sold.") + return True + + def set_cancelled_state(self): + if self.state != "sold": + self.state = "cancelled" + else: + raise UserError("Sold property can not be cancelled.") + return True + + @api.constrains("selling_price", "expected_price") + def _check_selling_price(self): + for record in self: + if ( + record.selling_price + and record.expected_price + and record.selling_price < record.expected_price * 0.9 + ): + raise ValidationError( + "The selling_price cannot be lower than 90% of the expected price" + ) + + @api.ondelete(at_uninstall=False) + def _unlink_property(self): + for rcd in self: + if rcd.state not in ("new", "cancelled"): + raise UserError("Only New and Cancelled property can be deleted.") diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..0be24b67c88 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,78 @@ +from odoo import api, fields, models +from dateutil.relativedelta import relativedelta +from odoo.exceptions import UserError + + +class PropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "Estate Property Offer" + _sql_constraints = [ + ( + "check_offer_price", + "CHECK(price > 0)", + "The Offer price of a property must be positive.", + ), + ] + _order = "price desc" + + price = fields.Float() + status = fields.Selection( + selection=[("accepted", "Accepted"), ("refused", "Refused")], copy=False + ) + partner_id = fields.Many2one("res.partner", required=True) + property_id = fields.Many2one("estate.property", required=True) + property_type_id = fields.Many2one(related="property_id.property_type_id") + validity = fields.Integer(default=7, help="Validity in days") + date_deadline = fields.Date( + compute="_compute_offer_deadline", inverse="_inverse_deadline", readonly=False + ) + + @api.depends("validity") + def _compute_offer_deadline(self): + for ofr in self: + if isinstance(ofr.create_date, bool): + ofr.date_deadline = fields.Date.today() + relativedelta( + days=ofr.validity + ) + else: + ofr.date_deadline = ofr.create_date + relativedelta(days=ofr.validity) + + def _inverse_deadline(self): + for ofr in self: + ofr.validity = (ofr.date_deadline - ofr.create_date.date()).days + + def accept_offer(self): + for offer in self: + other_accepted = offer.property_id.offer_ids.filtered( + lambda o: o.status == "accepted" and o != offer + ) + if other_accepted: + raise UserError("Only one offer can be accepted.") + + offer.status = "accepted" + offer.property_id.selling_price = offer.price + offer.property_id.buyer = offer.partner_id + offer.property_id.state = "offer_accepted" + return True + + def refuse_offer(self): + self.status = "refused" + return True + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + state = self.env["estate.property"].browse(vals["property_id"]).state + if state == "sold": + raise UserError("Cannot create an offer for a sold property") + best_offer = ( + self.env["estate.property"].browse(vals["property_id"]).best_price + ) + if vals["price"] > best_offer: + self.env["estate.property"].browse( + vals["property_id"] + ).state = "offer_received" + else: + errMes = f"The offer must be higher than {best_offer}" + raise UserError(errMes) + return super().create(vals_list) diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..b645f20966e --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,13 @@ +from odoo import fields, models + + +class PropertyTag(models.Model): + _name = "estate.property.tag" + _description = "Estate Property Tag" + _sql_constraints = [ + ("unique_property_tag_name", "UNIQUE(name)", "The name of tag must be unique."), + ] + _order = "name" + + name = fields.Char(required=True) + color = fields.Integer() diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..619477b096e --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,28 @@ +from odoo import api, fields, models + + +class PropertyType(models.Model): + _name = "estate.property.type" + _description = "Estate Property Type" + _sql_constraints = [ + ( + "unique_property_type_name", + "UNIQUE(name)", + "The name of tag must be unique.", + ), + ] + _order = "name" + + name = fields.Char(required=True) + property_ids = fields.One2many("estate.property", "property_type_id") + offer_ids = fields.One2many(related="property_ids.offer_ids") + offer_count = fields.Integer(compute="_compute_offer_count") + sequence = fields.Integer( + "Sequence", default=1, help="Used to order types. Lower is better." + ) + + @api.depends("offer_ids") + def _compute_offer_count(self): + for prp in self: + pricelist = prp.mapped("offer_ids") + prp.offer_count = len(pricelist) diff --git a/estate/models/inherited_estate.py b/estate/models/inherited_estate.py new file mode 100644 index 00000000000..dc2f73f194a --- /dev/null +++ b/estate/models/inherited_estate.py @@ -0,0 +1,11 @@ +from odoo import fields, models + + +class InheritedModel(models.Model): + _inherit = "res.users" + + property_ids = fields.One2many( + "estate.property", + "salesman", + domain=[("state", "in", ["new", "offer_received"])], + ) diff --git a/estate/report/estate_property_offers_template.xml b/estate/report/estate_property_offers_template.xml new file mode 100644 index 00000000000..46b1b2cb630 --- /dev/null +++ b/estate/report/estate_property_offers_template.xml @@ -0,0 +1,163 @@ + + + + + + + diff --git a/estate/report/estate_property_reports.xml b/estate/report/estate_property_reports.xml new file mode 100644 index 00000000000..3fe8b2d0e54 --- /dev/null +++ b/estate/report/estate_property_reports.xml @@ -0,0 +1,22 @@ + + + + Estate property offers/sale report + estate.property + qweb-pdf + estate.report_property_offers_document + estate.report_property_offers_document + + report + + + + Salesman's properties + res.users + qweb-pdf + estate.report_salesman_property_document + estate.report_salesman_property_document + + report + + diff --git a/estate/security/estate_property_rules.xml b/estate/security/estate_property_rules.xml new file mode 100644 index 00000000000..b7265dba26c --- /dev/null +++ b/estate/security/estate_property_rules.xml @@ -0,0 +1,24 @@ + + + + Agent: own or unassigned properties + + ['|', ('salesman', '=', False), ('salesman', '=', user.id)] + + + + + + + + + Estate Agent: Company Access Restriction + + [('company_id', '=', user.company_id.id)] + + + + + + + diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..9c95dac5fd7 --- /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 +access_estate_property_manager,access_estate_property_manager,model_estate_property,estate.estate_group_manager,1,1,1,0 +access_estate_property_user,access_estate_property_user,model_estate_property,estate.estate_group_user,1,1,1,0 +access_property_type_user,access_estate_property_type_user,model_estate_property_type,estate.estate_group_user,1,0,0,0 +access_property_tag_user,access_estate_property_tag_user,model_estate_property_tag,estate.estate_group_user,1,0,0,0 +access_property_type_manager,access_estate_property_type_manager,model_estate_property_type,estate.estate_group_manager,1,1,1,0 +access_property_tag_manager,access_estate_property_tag_manager,model_estate_property_tag,estate.estate_group_manager,1,1,1,0 +access_estate_property_offer,access_estate_property_offer,model_estate_property_offer,base.group_user,1,1,1,1 diff --git a/estate/security/security.xml b/estate/security/security.xml new file mode 100644 index 00000000000..2e2a62f9521 --- /dev/null +++ b/estate/security/security.xml @@ -0,0 +1,12 @@ + + + + Agent + + + + + Manager + + + diff --git a/estate/tests/__init__.py b/estate/tests/__init__.py new file mode 100644 index 00000000000..27788f94d44 --- /dev/null +++ b/estate/tests/__init__.py @@ -0,0 +1 @@ +from . import test_property_offer diff --git a/estate/tests/test_property_offer.py b/estate/tests/test_property_offer.py new file mode 100644 index 00000000000..878ab25291e --- /dev/null +++ b/estate/tests/test_property_offer.py @@ -0,0 +1,50 @@ +from odoo.tests.common import TransactionCase +from odoo.exceptions import UserError + + +class TestPropertyOfferRules(TransactionCase): + @classmethod + def setUpClass(self): + super().setUpClass() + self.property = self.env["estate.property"].create( + { + "name": "Test Villa", + "expected_price": 200000, + "state": "new", + } + ) + self.partner = self.env["res.partner"].create({"name": "Test Buyer"}) + + def test_cannot_create_offer_on_sold_property(self): + self.property.state = "sold" + with self.assertRaises(UserError): + self.env["estate.property.offer"].create( + { + "price": 180000, + "partner_id": self.partner.id, + "property_id": self.property.id, + } + ) + + def test_cannot_sell_without_accepted_offer(self): + self.env["estate.property.offer"].create( + { + "price": 180000, + "partner_id": self.partner.id, + "property_id": self.property.id, + } + ) + with self.assertRaises(UserError): + self.property.set_sold_state() + + def test_can_sell_with_accepted_offer(self): + self.env["estate.property.offer"].create( + { + "price": 190000, + "partner_id": self.partner.id, + "property_id": self.property.id, + "status": "accepted", + } + ) + self.property.set_sold_state() + self.assertEqual(self.property.state, "sold") diff --git a/estate/views/estate_menus_views.xml b/estate/views/estate_menus_views.xml new file mode 100644 index 00000000000..ea94bd9f244 --- /dev/null +++ b/estate/views/estate_menus_views.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + diff --git a/estate/views/estate_offers_views.xml b/estate/views/estate_offers_views.xml new file mode 100644 index 00000000000..53b354daa56 --- /dev/null +++ b/estate/views/estate_offers_views.xml @@ -0,0 +1,48 @@ + + + + estate.propertye.offer.form + estate.property.offer + +
+ + + + + + + + + +
+
+
+ + + + estate.property.offer.list + estate.property.offer + + + + + + + +

+ +

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

+ +

+ +
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + estate.property.search + estate.property + + + + + + + + + + + + + + + + + + + + + estate.property.list + estate.property + + + + + + + + + + + + + + + + + Properties + estate.property + list,form + {'search_default_available_properties': True, + 'search_default_current': True} + + + + Property Type + estate.property.type + list,form + + + + Property Tag + estate.property.tag + list,form + +
diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml new file mode 100644 index 00000000000..4ab3c7c86a0 --- /dev/null +++ b/estate/views/res_users_views.xml @@ -0,0 +1,15 @@ + + + + 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..fbde7dd1e48 --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,7 @@ +{ + "name": "Real Estate Account", + "depends": ["base", "estate", "account"], + "license": "LGPL-3", + "data": ["report/estate_property_inherited_template.xml"], + "application": True, +} 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..8381865223f --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,47 @@ +from odoo import models + + +class InheritedProperty(models.Model): + _inherit = "estate.property" + + def set_sold_state(self): + res = super().set_sold_state() + + for property in self: + property.check_access("write") + + if not property.buyer or not property.selling_price: + continue + + commission = property.selling_price * 0.06 + admin_fee = 100.0 + + self.env["account.move"].sudo().create( + { + "move_type": "out_invoice", + "partner_id": property.buyer.id, + "invoice_origin": property.name, + "invoice_line_ids": [ + ( + 0, + 0, + { + "name": "6% Commission", + "quantity": 1, + "price_unit": commission, + }, + ), + ( + 0, + 0, + { + "name": "Administrative Fees", + "quantity": 1, + "price_unit": admin_fee, + }, + ), + ], + } + ) + + return res diff --git a/estate_account/report/estate_property_inherited_template.xml b/estate_account/report/estate_property_inherited_template.xml new file mode 100644 index 00000000000..f8321027157 --- /dev/null +++ b/estate_account/report/estate_property_inherited_template.xml @@ -0,0 +1,13 @@ + + + + diff --git a/odoo_self_order_details/__init__.py b/odoo_self_order_details/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/odoo_self_order_details/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/odoo_self_order_details/__manifest__.py b/odoo_self_order_details/__manifest__.py new file mode 100644 index 00000000000..dba83b2586a --- /dev/null +++ b/odoo_self_order_details/__manifest__.py @@ -0,0 +1,16 @@ +{ + "name": "Odoo self order details", + "version": "1.0", + "depends": ["base", "pos_self_order"], + "license": "LGPL-3", + "assets": { + "pos_self_order.assets": [ + "odoo_self_order_details/static/src/**/*", + ], + }, + "data": [ + "views/product_template_view.xml", + ], + "installable": True, + "application": True, +} diff --git a/odoo_self_order_details/models/__init__.py b/odoo_self_order_details/models/__init__.py new file mode 100644 index 00000000000..e8fa8f6bf1e --- /dev/null +++ b/odoo_self_order_details/models/__init__.py @@ -0,0 +1 @@ +from . import product_template diff --git a/odoo_self_order_details/models/product_template.py b/odoo_self_order_details/models/product_template.py new file mode 100644 index 00000000000..cd58b439131 --- /dev/null +++ b/odoo_self_order_details/models/product_template.py @@ -0,0 +1,10 @@ +from odoo import models, fields + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + self_order_description = fields.Html( + string="Self Order Description", + translate=True, + ) diff --git a/odoo_self_order_details/static/src/components/product_card.js b/odoo_self_order_details/static/src/components/product_card.js new file mode 100644 index 00000000000..34902e6162b --- /dev/null +++ b/odoo_self_order_details/static/src/components/product_card.js @@ -0,0 +1,18 @@ +import { patch } from "@web/core/utils/patch"; +import { ProductCard } from "@pos_self_order/app/components/product_card/product_card"; + +patch(ProductCard.prototype, { + async selectProduct(qty = 1) { + const product = this.props.product; + + if (!product.self_order_available || !this.isAvailable) { + return; + } + + if (product.isCombo()) { + await super.selectProduct(qty); + } else { + this.router.navigate("product", { id: product.id }); + } + }, +}); diff --git a/odoo_self_order_details/static/src/components/product_page.js b/odoo_self_order_details/static/src/components/product_page.js new file mode 100644 index 00000000000..dd49128feb9 --- /dev/null +++ b/odoo_self_order_details/static/src/components/product_page.js @@ -0,0 +1,25 @@ +import { patch } from "@web/core/utils/patch"; +import { ProductPage } from "@pos_self_order/app/pages/product_page/product_page"; +import { markup } from "@odoo/owl"; +import { useService } from "@web/core/utils/hooks"; + +// Patch the ProductPage to include self_order_description +patch(ProductPage.prototype, { + async setup() { + super.setup(); + + const orm = useService("orm"); // or this.env.services.orm; + + const [rc] = await orm.read( + "product.product", + [this.props.product.id], + ["self_order_description"] + ); + + if (rc?.self_order_description) { + this.props.product.self_order_description = markup( + rc.self_order_description + ); + } + }, +}); diff --git a/odoo_self_order_details/static/src/xml/product_page.xml b/odoo_self_order_details/static/src/xml/product_page.xml new file mode 100644 index 00000000000..29db0fce669 --- /dev/null +++ b/odoo_self_order_details/static/src/xml/product_page.xml @@ -0,0 +1,36 @@ + + + + +

options

+
+ + +
+
+
+ Product image +
+
+
+

+

+

+ +
+ +
+ + + + +
+ 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..c2dbc46dba3 --- /dev/null +++ b/odoo_self_order_details/views/product_template_view.xml @@ -0,0 +1,15 @@ + + + + product.template.form.inherit.template + product.template + + + + + + + + + + diff --git a/sale_person/__manifest__.py b/sale_person/__manifest__.py new file mode 100644 index 00000000000..6b75948f295 --- /dev/null +++ b/sale_person/__manifest__.py @@ -0,0 +1,17 @@ +{ + "name": "Sale Person Attendance", + "version": "1.0", + "depends": ["base", "base_automation"], + "license": "LGPL-3", + "data": [ + "views/sale_person_models.xml", + "views/contact_fields.xml", + "views/sale_person_fields.xml", + "views/contact_views.xml", + "views/sale_person_views.xml", + "views/sale_person_menus.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..2c559fab1d3 --- /dev/null +++ b/sale_person/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_x_sale_person_sale_person_user,access_x_sale_person_sale_person_user,model_x_sale_person_sale_person,base.group_user,1,1,1,1 +access_x_sale_person_contact_user,access_x_sale_person_contact_user,model_x_sale_person_contact,base.group_user,1,1,1,1 diff --git a/sale_person/views/contact_fields.xml b/sale_person/views/contact_fields.xml new file mode 100644 index 00000000000..bb26a4f9673 --- /dev/null +++ b/sale_person/views/contact_fields.xml @@ -0,0 +1,38 @@ + + + + + x_name + char + Name + True + + + + + x_customer_type + char + Customer Type + + + + + x_city + char + City + + + + + x_area + char + Area + + + + + x_pincode + char + Pincode + + diff --git a/sale_person/views/contact_views.xml b/sale_person/views/contact_views.xml new file mode 100644 index 00000000000..9b5203d0e23 --- /dev/null +++ b/sale_person/views/contact_views.xml @@ -0,0 +1,45 @@ + + + + + + x.sale.person.contact.list + x_sale_person_contact + + + + + + + + + + + x.sale.person.contact.form + x_sale_person_contact + +
+ + + + + + + + + + + + + +
+
+
+ + + + Contact + x_sale_person_contact + list,form + +
diff --git a/sale_person/views/sale_person_fields.xml b/sale_person/views/sale_person_fields.xml new file mode 100644 index 00000000000..100634e1241 --- /dev/null +++ b/sale_person/views/sale_person_fields.xml @@ -0,0 +1,156 @@ + + + + + + x_sale_person_id + many2one + res.users + Sale Person + + + + + x_check_in + datetime + 1 + Check In + + + + + x_check_out + datetime + Check Out + + + + + x_customer + many2one + x_sale_person_contact + Customer + 1 + restrict + + + + + x_customer_type + char + Customer Type + x_customer.x_customer_type + + + + + x_city + char + City + x_customer.x_city + + + + + x_area + char + Area + x_customer.x_area + + + + x_pincode + char + Pincode + x_customer.x_pincode + + + + x_agenda + char + Agenda + + + + x_description + char + Description + + + + + x_conversion_possibilities + selection + + Conversion Possibilities + 1 + + + + + x_worked_hours + char + Worked Hours + + + + + x_checkin_location + char + CheckIn location + + + + + x_checkout_location + char + CheckOut location + + + + + + + Set Check-out Time + + + code + +checkout_time = datetime.datetime.now() +total_seconds = int((checkout_time - record.x_check_in).total_seconds()) +hours = total_seconds // 3600 +minutes = (total_seconds % 3600) // 60 +worked_hours_str = f"{hours:02}:{minutes:02}" + +record.write({ + 'x_check_out': checkout_time, + 'x_worked_hours': worked_hours_str, + 'x_checkout_location': 'Location Accessed [CheckOut]' +}) + + + + + Auto-fill Salesperson + + code + +record.write({ + 'x_check_in': datetime.datetime.now(), + 'x_worked_hours':'00:00', + 'x_checkin_location':'Location Accessed [CheckIn]' +}) + + + + + + Auto-fill Salesperson + + on_change + + + 1 + + diff --git a/sale_person/views/sale_person_menus.xml b/sale_person/views/sale_person_menus.xml new file mode 100644 index 00000000000..a3f03645dca --- /dev/null +++ b/sale_person/views/sale_person_menus.xml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/sale_person/views/sale_person_models.xml b/sale_person/views/sale_person_models.xml new file mode 100644 index 00000000000..0ecccd358c0 --- /dev/null +++ b/sale_person/views/sale_person_models.xml @@ -0,0 +1,17 @@ + + + + + + Sale person + x_sale_person_sale_person + manual + + + + + Sale person contact + x_sale_person_contact + manual + + diff --git a/sale_person/views/sale_person_views.xml b/sale_person/views/sale_person_views.xml new file mode 100644 index 00000000000..d121b23a78d --- /dev/null +++ b/sale_person/views/sale_person_views.xml @@ -0,0 +1,68 @@ + + + + + x.sale.person.list + x_sale_person_sale_person + + + + + + + + + + + + + + x.sale.person.form + x_sale_person_sale_person + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + Sale person + x_sale_person_sale_person + list,form + {'default_x_sale_person_id': uid} + +
diff --git a/sell_products_kit/__init__.py b/sell_products_kit/__init__.py new file mode 100644 index 00000000000..9b4296142f4 --- /dev/null +++ b/sell_products_kit/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard diff --git a/sell_products_kit/__manifest__.py b/sell_products_kit/__manifest__.py new file mode 100644 index 00000000000..5c39d53f71b --- /dev/null +++ b/sell_products_kit/__manifest__.py @@ -0,0 +1,15 @@ +{ + "name": "Sell products kit", + "version": "1.0", + "depends": ["sale_management"], + "license": "LGPL-3", + "data": [ + "views/product_template_view.xml", + "views/sales_order_view.xml", + "security/ir.model.access.csv", + "wizard/sub_product_wizard_view.xml", + "wizard/sub_product_wizard_line_view.xml", + ], + "installable": True, + "application": True, +} diff --git a/sell_products_kit/models/__init__.py b/sell_products_kit/models/__init__.py new file mode 100644 index 00000000000..8f2f8c0cbc1 --- /dev/null +++ b/sell_products_kit/models/__init__.py @@ -0,0 +1,3 @@ +from . import product_template +from . import sale_order_line +from . import sale_order diff --git a/sell_products_kit/models/product_template.py b/sell_products_kit/models/product_template.py new file mode 100644 index 00000000000..3d2ca0e82b8 --- /dev/null +++ b/sell_products_kit/models/product_template.py @@ -0,0 +1,8 @@ +from odoo import models, fields + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + is_kit = fields.Boolean() + sub_products_ids = fields.Many2many("product.product", string="Sub products") diff --git a/sell_products_kit/models/sale_order.py b/sell_products_kit/models/sale_order.py new file mode 100644 index 00000000000..9567647265b --- /dev/null +++ b/sell_products_kit/models/sale_order.py @@ -0,0 +1,35 @@ +from odoo import api, fields, models + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + print_in_report = fields.Boolean( + default=False, help="Print kits sub products in the report" + ) + + @api.onchange("order_line") + def _onchange_order(self): + current_kit_ids = {line._origin.id for line in self.order_line if line.product_is_kit} + + # filter out lines that are not kits and whose parent kit is not in the current kit ids (parent is_kit is deleted) + new_order_lines = self.order_line.filtered( + lambda line: not line.parent_kit_id.id + or (line.parent_kit_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: line.product_is_kit) + + def _get_invoiceable_lines(self, final=False): + invoicable_lines = super()._get_invoiceable_lines(final=final) + if self.print_in_report: + return invoicable_lines + else: + return invoicable_lines.filtered(lambda line: line.product_is_kit) diff --git a/sell_products_kit/models/sale_order_line.py b/sell_products_kit/models/sale_order_line.py new file mode 100644 index 00000000000..20531d0ae1f --- /dev/null +++ b/sell_products_kit/models/sale_order_line.py @@ -0,0 +1,28 @@ +from odoo import api, fields, models + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + product_is_kit = fields.Boolean( + compute="_compute_product_is_kit", + ) + parent_kit_id = fields.Many2one("sale.order.line") + original_price_unit = fields.Float() + + @api.depends("product_id") + def _compute_product_is_kit(self): + for line in self: + line.product_is_kit = line.product_id.product_tmpl_id.is_kit + if not line.original_price_unit: + line.original_price_unit = line.price_unit + + def open_sub_prod(self): + return { + "name": f"Product : {self.product_id.display_name}", + "type": "ir.actions.act_window", + "res_model": "sale.sub.product.wizard", + "view_mode": "form", + "target": "new", + "context": {"active_id": self.id}, + } diff --git a/sell_products_kit/security/ir.model.access.csv b/sell_products_kit/security/ir.model.access.csv new file mode 100644 index 00000000000..57f18af842b --- /dev/null +++ b/sell_products_kit/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id,perm_read,perm_write,perm_create,perm_unlink +access_sub_product_wizard_all,access.sub.product.wizard.all,model_sale_sub_product_wizard,,1,1,1,1 +access_sub_product_wizard_line_all,access.sub.product.wizard.line.all,model_sale_sub_product_wizard_line,,1,1,1,1 diff --git a/sell_products_kit/views/product_template_view.xml b/sell_products_kit/views/product_template_view.xml new file mode 100644 index 00000000000..7b59bf947d8 --- /dev/null +++ b/sell_products_kit/views/product_template_view.xml @@ -0,0 +1,18 @@ + + + + product.template.form.inherit.template + product.template + + + + + + + + + + + diff --git a/sell_products_kit/views/sales_order_view.xml b/sell_products_kit/views/sales_order_view.xml new file mode 100644 index 00000000000..3383709c31b --- /dev/null +++ b/sell_products_kit/views/sales_order_view.xml @@ -0,0 +1,43 @@ + + + + sale.order.form.inherited + sale.order + + + + + + + +