diff --git a/.gitignore b/.gitignore index b6e47617de1..8fdcc43f6c9 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,6 @@ dmypy.json # Pyre type checker .pyre/ + + +.vscode/ \ No newline at end of file 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..e173f862b67 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.js @@ -0,0 +1,37 @@ +import { Component, onWillStart, useState } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { Layout } from "@web/search/layout"; +import { useService } from "@web/core/utils/hooks"; +import { DashboardItem } from "./dashboard_item/dashboard_item"; +import { PieChart } from "./piechart/PieChart"; + +class AwesomeDashboard extends Component { + static template = "awesome_dashboard.AwesomeDashboard"; + static components = { Layout, DashboardItem, PieChart }; + + setup() { + this.action = useService("action"); + this.display = { + controlPanel: {}, + }; + this.statistics = useState(useService("awesome_dashboard.statistics")); + } + + openCustomer() { + this.action.doAction("base.action_partner_form"); + } + + openLead() { + this.action.doAction({ + type: "ir.actions.act_window", + name: "Leads", + res_model: "crm.lead", + views: [ + [false, "list"], + [false, "form"], + ], + }); + } +} + +registry.category("lazy_components").add("AwesomeDashboard", AwesomeDashboard); diff --git a/awesome_dashboard/static/src/dashboard/dashboard.scss b/awesome_dashboard/static/src/dashboard/dashboard.scss new file mode 100644 index 00000000000..e2234a6f3f1 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.scss @@ -0,0 +1,3 @@ +.o_dashboard { + background-color: rgb(46, 255, 108); +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml new file mode 100644 index 00000000000..f96a7344eaa --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard.xml @@ -0,0 +1,49 @@ + + + + + + + + + +
+ + Average Quantity of new orders this month +
+ +
+
+ + Average time for an order to go from 'new' to 'sent' or 'cancelled' +
+ +
+
+ + Number of new orders this month +
+ +
+
+ + Number of cancelled orders this month +
+ +
+
+ + Total amount of new orders this month +
+ +
+
+ + Shirt orders by size + + +
+
+
+ +
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..e69de29bb2d 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..32ffb966443 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.js @@ -0,0 +1,20 @@ +import { Component } from "@odoo/owl"; + +export class DashboardItem extends Component { + static template = "awesome_dashboard.DashboardItem"; + static props = { + size: { + type: Number, + optional: true, + default: 1, + }, + slots: { + type: Object, + optional: true, + } + } + + get cardStyle() { + return `width: ${this.props.size * 18}rem;`; + } +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.scss b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.scss new file mode 100644 index 00000000000..d232a817a2d --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.scss @@ -0,0 +1,9 @@ +.o_dashboard_card { + background-color: white; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + padding: 16px; + margin: 10px; + display: inline-block; + vertical-align: top; +} \ 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..b025433ac0d --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/dashboard_item/dashboard_item.xml @@ -0,0 +1,10 @@ + + + + +
+ +
+
+ +
diff --git a/awesome_dashboard/static/src/dashboard/number_card.js/number_card.js b/awesome_dashboard/static/src/dashboard/number_card.js/number_card.js new file mode 100644 index 00000000000..a504a0e3d06 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/number_card.js/number_card.js @@ -0,0 +1,15 @@ +import { Component } from "@odoo/owl"; + +export class NumberCard extends Component { + static template = "awesome_dashboard.NumberCard"; + static props = { + title: { + type: String, + optional: false, + }, + value: { + type: Number, + optional: false, + }, + } +} \ No newline at end of file diff --git a/awesome_dashboard/static/src/dashboard/number_card.js/number_card.xml b/awesome_dashboard/static/src/dashboard/number_card.js/number_card.xml new file mode 100644 index 00000000000..73bc3cac926 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/number_card.js/number_card.xml @@ -0,0 +1,9 @@ + + + + +
+ +
+
+
\ No newline at end of file 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..e56533235ac --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/piechart/PieChart.js @@ -0,0 +1,41 @@ +import { loadJS } from "@web/core/assets"; +import { getColor } from "@web/core/colors/colors"; +import { Component, onWillStart, useRef, onMounted, onWillUnmount } 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(); + }); + } + + 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, + }, + ], + }, + }); + } +} \ No newline at end of file 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..14e6684262c --- /dev/null +++ b/awesome_dashboard/static/src/dashboard/piechart/PieChart.xml @@ -0,0 +1,10 @@ + + + +
+
+ +
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard_loader.js b/awesome_dashboard/static/src/dashboard_loader.js new file mode 100644 index 00000000000..06ae51de339 --- /dev/null +++ b/awesome_dashboard/static/src/dashboard_loader.js @@ -0,0 +1,15 @@ +/** @odoo-module */ + +import { registry } from "@web/core/registry"; +import { LazyComponent } from "@web/core/assets"; +import { Component, xml } from "@odoo/owl"; + +class AwesomeDashboardLoader extends Component { + static components = { LazyComponent }; + static template = xml` + + `; + +} + +registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboardLoader); \ No newline at end of file diff --git a/awesome_dashboard/static/src/statistics_service.js b/awesome_dashboard/static/src/statistics_service.js new file mode 100644 index 00000000000..57040608df2 --- /dev/null +++ b/awesome_dashboard/static/src/statistics_service.js @@ -0,0 +1,24 @@ +import { registry } from "@web/core/registry"; +import { reactive } from "@odoo/owl"; +import { rpc } from "@web/core/network/rpc"; + +const statisticsService = { + + start(env) { + const statistics = reactive({ isReady: false }); + + async function loadData() { + const updates = await rpc("/awesome_dashboard/statistics"); + Object.assign(statistics, updates, { isReady: true }); + } + + setInterval(loadData, 10 * 60 * 1000); + loadData(); + + return statistics; + }, +}; + +registry + .category("services") + .add("awesome_dashboard.statistics", statisticsService); 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..b57450e685b --- /dev/null +++ b/awesome_owl/static/src/Todo/todo_item.js @@ -0,0 +1,28 @@ +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, default: 0 }, + description: { type: String }, + isCompleted: { type: Boolean }, + }, + }, + removeTodo: { type: Function, optional: true }, + toggleState: { type: Function, optional: true }, + }; + + onToggle() { + if (this.props.toggleState) { + this.props.toggleState(this.props.todo.id); + } + } + onRemoveTodo() { + if (this.props.removeTodo) { + 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..b1214b17f35 --- /dev/null +++ b/awesome_owl/static/src/Todo/todo_item.xml @@ -0,0 +1,19 @@ + + + +
  • +
    + + # + + + + + +
    + + + +
  • +
    +
    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..ca9f70aa7fb --- /dev/null +++ b/awesome_owl/static/src/Todo/todo_list.js @@ -0,0 +1,41 @@ +import { Component, useState } from "@odoo/owl"; +import { TodoItem } from "./todo_item"; + +export class TodoList extends Component { + static template = "awesome_owl.todo_list"; + static components = { TodoItem }; + static props = {}; + + setup() { + this.todos = useState([]); + } + + toggleState = (id) => { + const todo = this.todos.find((t) => t.id === id); + if (todo) { + todo.isCompleted = !todo.isCompleted; + } + }; + + addTodo(ev) { + if (ev.keyCode !== 13) return; + const description = ev.target.value.trim(); + + if (!description) return; + + this.todos.push({ + id: this.todos.length + 1, + description: description, + isCompleted: false, + }); + + ev.target.value = ""; + } + + onRemoveTodo = (todoId) => { + const index = this.todos.findIndex((elem) => elem.id === todoId); + 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..5c43f78416d --- /dev/null +++ b/awesome_owl/static/src/Todo/todo_list.xml @@ -0,0 +1,18 @@ + + + +
    +
    +
    +

    📝 My Todo List

    + +
      + + + +
    +
    +
    +
    +
    +
    diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js new file mode 100644 index 00000000000..eacc3d83ee0 --- /dev/null +++ b/awesome_owl/static/src/card/card.js @@ -0,0 +1,16 @@ +import { Component } from "@odoo/owl"; + +export class Card extends Component { + static template = "awesome_owl.card"; + static props = { + title: { type: String, optional: true }, + content: { type: String, optional: true }, + slots: {}, + }; + + setup() { + this.state = { + isOpen: true, + }; + } +} diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml new file mode 100644 index 00000000000..ad764e3573b --- /dev/null +++ b/awesome_owl/static/src/card/card.xml @@ -0,0 +1,16 @@ + + + +
    +
    +
    + +
    +

    + +

    + +
    +
    +
    +
    \ No newline at end of file diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js new file mode 100644 index 00000000000..e53ed0e69ad --- /dev/null +++ b/awesome_owl/static/src/counter/counter.js @@ -0,0 +1,21 @@ +import { Component, useState } from "@odoo/owl"; + +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 += 1; + if (this.props.onChange) { + this.props.onChange(this.state.value); + } + } +} diff --git a/awesome_owl/static/src/counter/counter.xml b/awesome_owl/static/src/counter/counter.xml new file mode 100644 index 00000000000..7292abd0caa --- /dev/null +++ b/awesome_owl/static/src/counter/counter.xml @@ -0,0 +1,10 @@ + + + +
    +

    Counter: +

    + +
    +
    +
    \ No newline at end of file diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 657fb8b07bb..38f3c9dda56 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,7 +1,27 @@ -/** @odoo-module **/ - -import { Component } from "@odoo/owl"; +import { Component, markup, useState } from "@odoo/owl"; +import { Counter } from "./counter/counter"; +import { Card } from "./card/card"; +import { TodoList } from "./Todo/todo_list"; export class Playground extends Component { - static template = "awesome_owl.playground"; + static template = "awesome_owl.playground"; + static components = { Counter, Card, TodoList }; + static props = []; + + setup() { + this.state = useState({ + sum: 0, + }); + } + + incrementSum = () => { + this.state.sum++; + }; + + toggle = () => { + this.state.isOpen = !this.state.isOpen; + }; + + htmlString = "
    This is red text
    "; + safeHtmlString = markup("
    This is green text
    "); } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..da060b84e5d 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -1,10 +1,33 @@ - -
    +
    hello world +
    + + +
    - +
    + +

    This is plain text inside a card.

    +
    + + + + + + + +
    +

    Sum: +

    - +
    +

    Todo App

    + +
    + + 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..1c6bee8fdd9 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,28 @@ +{ + 'name': 'Estate', + 'version': '1.0', + 'license': 'LGPL-3', + 'summary': 'Real estate management module', + 'description': 'Manage properties, owners, and sales in your real estate agency', + 'category': 'Real Estate', + 'author': 'ksoz', + 'depends': ['base'], + 'data': [ + 'security/security.xml', + 'security/ir.model.access.csv', + 'data/estate.property.type.csv', + 'views/estate_property_offer_views.xml', + 'views/estate_property_types_views.xml', + 'views/estate_property_views.xml', + 'views/estate_property_tags_views.xml', + "views/estate_res_users_views.xml", + 'views/estate_menus.xml', + 'report/estate_property_templates.xml', + 'report/estate_property_report.xml', + ], + 'demo': [ + 'demo/property_demo_data.xml', + ], + 'installable': True, + 'application': True, +} diff --git a/estate/data/estate.property.type.csv b/estate/data/estate.property.type.csv new file mode 100644 index 00000000000..4d0d7a60f78 --- /dev/null +++ b/estate/data/estate.property.type.csv @@ -0,0 +1,5 @@ +"id","name" +property_type_residential,"Residential" +property_type_commercial,"Commercial" +property_type_industrial,"Industrial" +property_type_land,"Land" diff --git a/estate/demo/property_demo_data.xml b/estate/demo/property_demo_data.xml new file mode 100644 index 00000000000..6d4ab378213 --- /dev/null +++ b/estate/demo/property_demo_data.xml @@ -0,0 +1,56 @@ + + + Residential + + + Big Villa + A nice and big villa + 12345 + + 1600000.0 + 6 + 100 + 4 + + + 100000 + south + + + + Trailer home + cancelled + Home in a trailer park + 54321 + + 100000.0 + 99000.0 + 1 + 10 + 4 + + + + + + Apartment with Inline Offers + 250000.0 + + + + + + + 1500000 + + + + + 1500001 + + + + + 1500002 + + \ No newline at end of file diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..03aa38b51c0 --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1,5 @@ +from . import estate_property +from . import estate_property_type +from . import estate_property_tags +from . import estate_property_offer +from . import estate_res_users diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..9468a5dbdda --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,120 @@ +from odoo import api, fields, models +from odoo.exceptions import UserError, ValidationError +from odoo.tools.float_utils import float_compare, float_is_zero + + +class EstateProperty(models.Model): + _name = "estate.property" + _description = "Real Estate Property" + _order = "id desc" + + name = fields.Char(required=True) + description = fields.Text(string="Description") + postcode = fields.Char(string="PostCode") + date_availability = fields.Date( + string="Available From", + copy=False, + default=lambda self: fields.Date.add(fields.Date.today(), months=3), + ) + expected_price = fields.Float(required=True, string="Expected Price") + selling_price = fields.Float(string="Selling Price") + bedrooms = fields.Integer(string="Bedroom") + living_area = fields.Integer(string="Living Area") + facades = fields.Integer(string="Facades") + garage = fields.Boolean(string="Garage") + garden = fields.Boolean(string="Garden") + garden_area = fields.Integer(string="Garden Area") + garden_orientation = fields.Selection( + [("north", "North"), ("south", "South"), ("east", "East"), ("west", "West")], + string="Garden Orientation", + ) + active = fields.Boolean(default=True) + state = fields.Selection( + [ + ("new", "New"), + ("offer_received", "Offer Received"), + ("offer_accepted", "Offer Accepted"), + ("sold", "Sold"), + ("cancelled", "Cancelled"), + ], + string="Status", + copy=False, + required=True, + default="new", + ) + property_type_id = fields.Many2one("estate.property.type", string="Property Type") + seller_id = fields.Many2one( + "res.users", string="Salesman", default=lambda self: self.env.user + ) + buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False) + tag_ids = fields.Many2many("estate.property.tag", string="Tags") + offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers") + total_area = fields.Integer(compute="_compute_total_area") + best_offer = fields.Float(compute="_compute_best_offer") + company_id = fields.Many2one('res.company', string="Company", default=lambda self: self.env.company) + + _sql_constraints = [ + ( + "expected_price", + "CHECK(expected_price >= 0)", + "The expected price must be strictly positive.", + ) + ] + + @api.depends("living_area", "garden_area") + def _compute_total_area(self): + for record in self: + record.total_area = record.living_area + record.garden_area + + @api.depends("offer_ids.price") + def _compute_best_offer(self): + for record in self: + if record.offer_ids: + record.best_offer = max(record.offer_ids.mapped("price")) + else: + record.best_offer = 0.0 + + @api.onchange("garden") + def _onchange_garden(self): + if self.garden: + self.garden_area = 10 + self.garden_orientation = "north" + else: + self.garden_area = 0 + self.garden_orientation = False + + def action_on_sold(self): + for record in self: + if record.state == "cancelled": + raise UserError("A cancelled property cannot be sold") + record.state = "sold" + return True + + def action_on_cancelled(self): + for record in self: + if record.state == "sold": + raise UserError("A sold property cannot be cancelled.") + record.state = "cancelled" + return True + + @api.constrains("selling_price", "expected_price") + def _check_selling_price(self): + for record in self: + if float_is_zero(record.selling_price, precision_digits=2): + continue + + min_acceptable = record.expected_price * 0.9 + + if ( + float_compare(record.selling_price, min_acceptable, precision_digits=2) + < 0 + ): + raise ValidationError( + "Selling price must be at least 90% of the expected price." + ) + + @api.ondelete(at_uninstall=False) + def _check_before_delete(self): + for record in self: + if record.state not in ('new', 'cancelled'): + raise UserError("You can only delete properties that are 'New' or 'Cancelled'.") diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..89d61f65f73 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,78 @@ +from odoo import api, fields, models +from odoo.exceptions import UserError + + +class EstatePropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "Offers for estate property" + _order = "price desc" + + price = fields.Float() + partner_id = fields.Many2one("res.partner", string="Partner", required=True) + property_id = fields.Many2one("estate.property", string="Property", required=True) + validity = fields.Integer(string="Validity (days)", default=7) + date_deadline = fields.Date( + string="Deadline", compute="_compute_date_deadline", inverse="_inverse_date_deadline", + ) + status = fields.Selection( + selection=[("accepted", "Accepted"), ("refused", "Refused")], string="Status", copy=False, + ) + property_type_id = fields.Many2one( + 'estate.property.type', related='property_id.property_type_id', string="Property Type", store=True + ) + + _sql_constraints = [ + ( + "check_offer_price_positive", + "CHECK(price > 0)", + "Offer price must be strictly positive.", + ), + ] + + @api.depends("validity") + def _compute_date_deadline(self): + for record in self: + start_date = record.create_date or fields.Date.context_today(record) + record.date_deadline = fields.Date.add(start_date, days=record.validity) + + def _inverse_date_deadline(self): + for record in self: + start_date = record.create_date or fields.Date.context_today(record) + if record.date_deadline: + delta = record.date_deadline - start_date.date() + record.validity = delta.days + else: + record.validity = 0 + + def action_on_accepted(self): + for offer in self: + accepted_offers = offer.property_id.offer_ids.filtered( + lambda o: o.status == "accepted" + ) + if accepted_offers: + raise UserError("An offer has already been accepted for this property.") + + offer.status = "accepted" + offer.property_id.selling_price = offer.price + offer.property_id.buyer_id = offer.partner_id + offer.property_id.state = "offer_accepted" + return True + + def action_on_refused(self): + for offer in self: + offer.status = "refused" + return True + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + prop = self.env['estate.property'].browse(vals.get('property_id')) + + if prop.offer_ids: + max_prop = max(prop.offer_ids.mapped('price')) + if vals['price'] < max_prop: + raise UserError(f"The offer price must be higher than the current best offer of {max_prop}.") + + prop.state = 'offer_received' + + return super().create(vals_list) diff --git a/estate/models/estate_property_tags.py b/estate/models/estate_property_tags.py new file mode 100644 index 00000000000..d35a22f12e3 --- /dev/null +++ b/estate/models/estate_property_tags.py @@ -0,0 +1,14 @@ +from odoo import fields, models + + +class EstatePropertyTag(models.Model): + _name = "estate.property.tag" + _description = "Tags For Estate Properties" + _order = "name" + + name = fields.Char(required=True, string="Tag") + color = fields.Integer('Color') + + _sql_constraints = [ + ("unique_tag_name", "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..b45ffa55eb0 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,25 @@ +from odoo import fields, models + + +class EstatePropertyType(models.Model): + _name = "estate.property.type" + _description = "Types of Real Estate Properties" + _order = "sequence,name" + + name = fields.Char(required=True, string="Property Type") + property_ids = fields.One2many( + "estate.property", "property_type_id", string="Properties" + ) + sequence = fields.Integer("Sequence") + offer_ids = fields.One2many( + "estate.property.offer", "property_type_id", string="Offers" +) + offer_count = fields.Integer(string="Offers Count", compute="_compute_offer_count") + + _sql_constraints = [ + ("unique_type_name", "UNIQUE(name)", "Property type name must be unique."), + ] + + def _compute_offer_count(self): + for record in self: + record.offer_count = len(record.offer_ids) diff --git a/estate/models/estate_res_users.py b/estate/models/estate_res_users.py new file mode 100644 index 00000000000..f9fc770c11a --- /dev/null +++ b/estate/models/estate_res_users.py @@ -0,0 +1,9 @@ +from odoo import fields, models + + +class ResUsers(models.Model): + _inherit = "res.users" + + property_ids = fields.One2many( + 'estate.property', 'seller_id', string="Properties", domain="[('state', 'in', ['new', 'offer_received'])]" + ) diff --git a/estate/report/estate_property_report.xml b/estate/report/estate_property_report.xml new file mode 100644 index 00000000000..d5dc507bed7 --- /dev/null +++ b/estate/report/estate_property_report.xml @@ -0,0 +1,21 @@ + + + + + Property Offers + estate.property + qweb-pdf + estate.report_property_offers_document estate.report_property_offers_document 'Property Offers Report for %s' % object.name + report + + + + + Salesman Properties res.users + qweb-pdf + estate.report_salesman_properties + estate.report_salesman_properties + 'Salesman Property Offers for %s' % object.name + report + + diff --git a/estate/report/estate_property_templates.xml b/estate/report/estate_property_templates.xml new file mode 100644 index 00000000000..40bf6e00d71 --- /dev/null +++ b/estate/report/estate_property_templates.xml @@ -0,0 +1,161 @@ + + + + + + + diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..9fc6d823dc3 --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,10 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_estate_property_manager,access_estate_property_manager,model_estate_property,estate_group_manager,1,1,1,0 +access_estate_property_type_manager,access_estate_property_type_manager,model_estate_property_type,estate_group_manager,1,1,1,1 +access_estate_property_tag_manager,access_estate_property_tag_manager,model_estate_property_tag,estate_group_manager,1,1,1,1 +access_estate_offer_manager,access_estate_offer_manager,model_estate_property_offer,estate_group_manager,1,1,1,1 + +access_estate_property_user,access_estate_property_user,model_estate_property,estate_group_user,1,1,1,0 +access_estate_property_type_user,access_estate_property_type_user,model_estate_property_type,estate_group_user,1,1,0,0 +access_estate_property_tag_user,access_estate_property_tag_user,model_estate_property_tag,estate_group_user,1,0,0,0 +access_estate_property_offer_user,access_estate_property_offer_user,model_estate_property_offer,estate_group_user,1,1,1,1 diff --git a/estate/security/security.xml b/estate/security/security.xml new file mode 100644 index 00000000000..55abc0f98cb --- /dev/null +++ b/estate/security/security.xml @@ -0,0 +1,48 @@ + + + Agent + + + + + Manager + + + + + + + Agents: Own or Unassigned Properties Only + + + [ + '|', + ('seller_id', '=', user.id), + ('seller_id', '=', False) + ] + + + + + + + Managers: Full Access to Properties + + + [(1, '=', 1)] + + + + + + + Estate Property Multi-company + + [ + ('company_id', 'in', company_ids) + ] + + + + + \ No newline at end of file diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..514cf9107b6 --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..02dc7e4a0b4 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,46 @@ + + + + estate.property.offer.list + estate.property.offer + + + + + + + + +
    + +
    +

    + +

    +
    + + + + + + + + + + + +
    + + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..c3036cddd5b --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,117 @@ + + + + Properties + estate.property + list,form + {'search_default_available': True} + + + + estate.property.list + estate.property + + + + + + + + + + + + + + + + estate.property.form + estate.property + +
    +
    +
    + +

    + +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + +
    +
    + + + estate.property.view.search + estate.property + + + + + + + + + + + + + + + +
    diff --git a/estate/views/estate_res_users_views.xml b/estate/views/estate_res_users_views.xml new file mode 100644 index 00000000000..a6d88ee5629 --- /dev/null +++ b/estate/views/estate_res_users_views.xml @@ -0,0 +1,15 @@ + + + + res.users.form.inherited.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..b63acc4b968 --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,19 @@ +{ + "name": "Estate Account", + "version": "1.0", + "author": "ksoz", + "license": "LGPL-3", + "category": "Real Estate/Brokerage", + "description": """ + This module links the Real Estate module with the Accounting module. + """, + "depends": [ + "estate", + "account", + ], + "data": [ + ], + "installable": True, + "application": False, + "auto_install": False, +} 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..642cac09f55 --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,33 @@ +from odoo import models, Command + + +class EstateProperty(models.Model): + _inherit = "estate.property" + + def action_on_sold(self): + res = super().action_on_sold() + + for record in self: + invoice_vals = { + 'partner_id': record.buyer_id.id, + 'move_type': 'out_invoice', + 'invoice_line_ids': [ + Command.create( + { + "name": self.name, + "quantity": 1, + "price_unit": 0.06 * self.selling_price, + } + ), + Command.create( + { + "name": "administrative fees", + "quantity": 1, + "price_unit": 100.00, + } + ), + ], + } + self.env['account.move'].create(invoice_vals) + + return res