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 @@ + + + + + + + Customers + Leads + + + + + + + + + + + + + + + + + + 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 + + + Apply + + + + 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: + + Increment + + + + 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 @@ + + + + + + + + + + + + Address: + + + + + + + + + , + + , + + + + + + + + + + + + + + + + + Salesman: + + + Expected Price: + + + Status: + + + + + + + + No offer has been made yet !:) + + + + + + + + + + + + + + + + + + + + + + + + + + , + + , + + + + + + + + + + + Salesman: + + + + + + + + + + + + + + Expected Price: + + + Status: + + + + + + + + No offer has been made yet !:) + + + + + + + + + + + + + + + Price + Partner + Validity(days) + Deadline + State + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + + + + + + + + + + + + + + Property Offers + estate.property.offer + list,form + [('property_type_id', '=', active_id)] + + diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml new file mode 100644 index 00000000000..c826fa3949b --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,54 @@ + + + + estate.propertye.type.form + estate.property.type + + + + + + + + + + + + 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 @@ + + + + + + + Invoice has already been created!!! + + + + +
T-shirts Sales by size
Counter:
+ Invoice has already been created!!! +