diff --git a/.gitignore b/.gitignore index b6e47617de1..729df8f7a2a 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,6 @@ dmypy.json # Pyre type checker .pyre/ + +# IDE Settings +.vscode/ diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000000..f5f5da21938 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,56 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python Debugger: Current File", + "type": "debugpy", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal" + }, + + { + "name": "Odoo Server", + "type": "debugpy", + "request": "launch", + "python": "/home/odoo/odoo/venv/bin/python3", + "program": "/home/odoo/odoo/odoo-bin", + "args" : [ + "--addons-path", "/home/odoo/odoo/addons/,/home/enterprise/,/home/tutorials", + "-d", "rd_demo", + "--dev", "all", + "--without-demo", "all", + "-p", "8069", + "--limit-time-cpu", "0", + "--limit-time-real", "0", + // "-i", "", + ], + "console": "integratedTerminal", + "variablePresentation": {} + }, + { + "name": "Test Odoo", + // "preLaunchTask": "drop_test_db", + "type": "debugpy", + "request": "launch", + "program": "/home/odoo/odoo/odoo-bin", + "args" : [ + "--addons-path", "/home/odoo/odoo/addons/,/home/enterprise/,/home/tutorials", + "-d", "DB_NAMEtest", + "-p", "8071", + "--limit-time-cpu", "0", + "--limit-time-real", "0", + "--without-demo", "all", + "--stop-after-init", + //"-i", "", + "--test-tags", "", + //"--log-level", "error", + ], + "console": "integratedTerminal", + "variablePresentation": {} + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000000..7ae3cd80ae9 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,59 @@ +{ + "folders": [ + { + "path": "/home/odoo/odoo" + }, + { + "path": "/home/enterprise" + }, + { + "path": "/home/tutorials" + } + ], + "settings": { + "launch": { + "version": "0.2.0", + "configurations": [ + { + "name": "Odoo Server", + "type": "debugpy", + "request": "launch", + "program": "/home/odoo/odoo/odoo-bin", + "args" : [ + "--addons-path", "/home/odoo/odoo/addons/,/home/enterprise/,/home/tutorials", + "-d", "rd_demo", + "--dev", "all", + "--without-demo", "all", + "-p", "8069", + "--limit-time-cpu", "0", + "--limit-time-real", "0", + // "-i", "", + ], + "console": "integratedTerminal", + "variablePresentation": {} + }, + { + "name": "Test Odoo", + // "preLaunchTask": "drop_test_db", + "type": "debugpy", + "request": "launch", + "program": "/home/odoo/odoo/odoo-bin", + "args" : [ + "--addons-path", "/home/odoo/odoo/addons/,/home/enterprise/,/home/tutorials", + "-d", "DB_NAMEtest", + "-p", "8071", + "--limit-time-cpu", "0", + "--limit-time-real", "0", + "--without-demo", "all", + "--stop-after-init", + //"-i", "", + "--test-tags", "", + //"--log-level", "error", + ], + "console": "integratedTerminal", + "variablePresentation": {} + } + ] + } + } +} \ No newline at end of file diff --git a/awesome_dashboard/controllers/controllers.py b/awesome_dashboard/controllers/controllers.py index 56d4a051287..05977d3bd7f 100644 --- a/awesome_dashboard/controllers/controllers.py +++ b/awesome_dashboard/controllers/controllers.py @@ -9,7 +9,7 @@ logger = logging.getLogger(__name__) class AwesomeDashboard(http.Controller): - @http.route('/awesome_dashboard/statistics', type='json', auth='user') + @http.route('/awesome_dashboard/statistics', type='jsonrpc', auth='user') def get_statistics(self): """ Returns a dict of statistics about the orders: diff --git a/awesome_owl/__manifest__.py b/awesome_owl/__manifest__.py index e8ac1cda552..55002ab81de 100644 --- a/awesome_owl/__manifest__.py +++ b/awesome_owl/__manifest__.py @@ -29,8 +29,10 @@ 'assets': { 'awesome_owl.assets_playground': [ ('include', 'web._assets_helpers'), + ('include', 'web._assets_backend_helpers'), 'web/static/src/scss/pre_variables.scss', 'web/static/lib/bootstrap/scss/_variables.scss', + 'web/static/lib/bootstrap/scss/_maps.scss', ('include', 'web._assets_bootstrap'), ('include', 'web._assets_core'), 'web/static/src/libs/fontawesome/css/font-awesome.css', diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js new file mode 100644 index 00000000000..fa424fe9fc3 --- /dev/null +++ b/awesome_owl/static/src/card/card.js @@ -0,0 +1,20 @@ +import { Component, useState } from "@odoo/owl"; + +export class Card extends Component { + static template = "awesome_owl.card"; + static props = { + title: String, + slots: { + type: Object, + optional: true, + }, + }; + + setup() { + this.state = useState({ isOpen: true }); + } + + toggle() { + this.state.isOpen = !this.state.isOpen; + } +} diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml new file mode 100644 index 00000000000..50a6df93d9f --- /dev/null +++ b/awesome_owl/static/src/card/card.xml @@ -0,0 +1,19 @@ + + + +
+
+
+
+ +
+ +
+ +
+
+
+
+
\ 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..88c50905228 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.js @@ -0,0 +1,19 @@ +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++; + if (this.props.onChange) { + this.props.onChange(); + } + } +} diff --git a/awesome_owl/static/src/counter/counter.xml b/awesome_owl/static/src/counter/counter.xml new file mode 100644 index 00000000000..b2c2308ca74 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.xml @@ -0,0 +1,11 @@ + + + +
+

Counter:

+ +
+
+
diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 4ac769b0aa5..bad284b0e18 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,5 +1,18 @@ -import { Component } from "@odoo/owl"; +import { Component, useState, markup } from "@odoo/owl"; +import { Counter } from "./counter/counter"; +import { Card } from "./card/card"; +import { TodoList } from "./todo/todoList"; export class Playground extends Component { static template = "awesome_owl.playground"; + static components = { Card, Counter, TodoList }; + + setup() { + this.htmlContent = markup("some content!"); + this.state = useState({ sum: 2 }); + } + + incrementSum = () => { + this.state.sum++; + } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..7c1a8ed0e6d 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -1,10 +1,15 @@ -
hello world
+ + + + + + Sum: +
-
diff --git a/awesome_owl/static/src/todo/todoItem.js b/awesome_owl/static/src/todo/todoItem.js new file mode 100644 index 00000000000..de58f099d12 --- /dev/null +++ b/awesome_owl/static/src/todo/todoItem.js @@ -0,0 +1,26 @@ +import { Component } from "@odoo/owl"; + +export class TodoItem extends Component { + static template = "awesome_owl.todoItem"; + static props = { + todo: { + type: Object, + shape: { + id: Number, + description: String, + isCompleted: Boolean, + }, + }, + toggleTodo: Function, + removeTodo: Function + }; + + + onToggleState() { + this.props.toggleTodo(this.props.todo.id); + } + + onDelete(){ + this.props.removeTodo(this.props.todo.id) + } +} diff --git a/awesome_owl/static/src/todo/todoList.js b/awesome_owl/static/src/todo/todoList.js new file mode 100644 index 00000000000..0b4e1aba93d --- /dev/null +++ b/awesome_owl/static/src/todo/todoList.js @@ -0,0 +1,39 @@ +import { Component, useState, useRef, onMounted } from "@odoo/owl"; +import { TodoItem } from "./todoItem"; +import { useAutofocus } from "../utils/useAutofocus"; + +export class TodoList extends Component { + static template = "awesome_owl.todoList"; + static components = { TodoItem }; + + setup() { + this.nextId = 1 + this.todos = useState([]); + this.inputRef = useAutofocus('todo_input'); + } + + addTodo(ev) { + if (ev.keyCode === 13 && ev.target.value.trim() !== "") { + this.todos.push({ + id: this.nextId++, + description: ev.target.value, + isCompleted: false, + }); + ev.target.value = ""; + } + } + + toggleTodo = (todoId) => { + const todo = this.todos.find((t) => t.id === todoId); + if (todo) { + todo.isCompleted = !todo.isCompleted; + } + } + + removeTodo = (todoId) => { + const index = this.todos.findIndex((t) => t.id === todoId); + if (index >= 0) { + this.todos.splice(index, 1); + } + } +} 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..358d14e3d33 --- /dev/null +++ b/awesome_owl/static/src/todo/todo_item.xml @@ -0,0 +1,21 @@ + + + +
+ + + + +
+
+
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..c99b58ac7e9 --- /dev/null +++ b/awesome_owl/static/src/todo/todo_list.xml @@ -0,0 +1,17 @@ + + + +
+

To-do List

+ + + + +
+
+
diff --git a/awesome_owl/static/src/utils/useAutofocus.js b/awesome_owl/static/src/utils/useAutofocus.js new file mode 100644 index 00000000000..04d49b25a00 --- /dev/null +++ b/awesome_owl/static/src/utils/useAutofocus.js @@ -0,0 +1,12 @@ +import { useRef, onMounted } from "@odoo/owl"; + + +export function useAutofocus(name) { + const ref = useRef(name); + onMounted(() => { + if (ref.el) { + ref.el.focus(); + } + }); + return ref; +} 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..e35f2fd70f3 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,20 @@ +{ + 'name': "Estate", + 'version': '19.0.0.1.0', + 'depends': [ + 'base', + ], + 'category': 'Tutorials', + 'application': True, + 'data': [ + 'security/ir.model.access.csv', + 'views/res_users_views.xml', + 'views/estate_property_views.xml', + 'views/estate_property_offer_views.xml', + 'views/estate_property_type_views.xml', + 'views/estate_property_tag_views.xml', + 'views/estate_menus.xml', + ], + 'author': "Odoo", + 'license': 'AGPL-3' +} diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..8f914bbb526 --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1,5 @@ +from . import estate_property +from . import estate_property_type +from . import estate_property_tag +from . import estate_property_offer +from . import res_user diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..f2ab8f6366b --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,126 @@ +from odoo import api, exceptions, fields, models +from odoo.exceptions import UserError, ValidationError +from odoo.tools import float_compare + + +class EstatePropertytModel(models.Model): + _name = 'estate.property' + _description = 'Estate Property Model' + _order = 'id desc' + + name = fields.Char(required=True) + description = fields.Text() + postcode = fields.Char() + date_availability = fields.Date(copy=False, default=lambda self: fields.Date.add(fields.Date.today(), months=3)) + 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() + garden_orientation = fields.Selection( + string='Type', + selection=[ + ('north', 'North'), + ('south', 'South'), + ('east', 'East'), + ('west', 'West'), + ] + ) + active = fields.Boolean(default=True) + state = fields.Selection( + selection=[ + ('new', 'New'), + ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('cancelled', 'Cancelled'), + ], + required=True, + copy=False, + default='new', + ) + type_id = fields.Many2one( + 'estate.property.type', + string='Tag', + ) + salesperson_id = fields.Many2one( + 'res.users', + string='Salesperson', + 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_price = fields.Float(compute='_compute_best_price') + + @api.depends('living_area', 'garden_area') + def _compute_total_area(self): + for record in self: + record.total_area = (record.living_area or 0) + (record.garden_area or 0) + + @api.depends('offer_ids') + def _compute_best_price(self): + for record in self: + record.best_price = max(record.offer_ids.mapped('price'), default=0.0) + + @api.onchange('garden') + def _onchange_garden(self): + if self.garden: + self.garden_area = 10 + self.garden_orientation = 'north' + else: + self.garden_area = 0 + self.garden_orientation = '' + + def action_sold(self): + for record in self: + if record.state == 'cancelled': + raise exceptions.UserError('Cancelled properties cannot be sold.') + record.state = 'sold' + + def action_cancel(self): + for record in self: + if record.state == 'sold': + raise exceptions.UserError("Sold properties can't be cancelled.") + record.state = 'cancelled' + + @api.constrains('selling_price', 'expected_price') + def _check_expected_to_selling_price_ratio(self): + for record in self: + limit = record.expected_price * 0.9 + if float_compare(record.selling_price, limit, precision_digits=2) == -1: + raise ValidationError('The selling price cannot be lower than 90% of the expected price!') + + _check_expected_price = models.Constraint( + 'CHECK(expected_price > 0)', + 'The Expected price of a property should be > 0', + ) + _check_selling_price = models.Constraint( + 'CHECK(selling_price > 0)', + 'The selling price of a property should be > 0', + ) + _check_name_unique = models.Constraint( + 'UNIQUE(name)', + 'The prop name must be unique!', + ) + + @api.ondelete(at_uninstall=False) + def _check_property_state_on_delete(self): + for record in self: + if record.state not in ['new', 'canceled']: + raise UserError("You cannot delete a property that is not '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..d81ad890395 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,91 @@ +from odoo import api, exceptions, fields, models +from odoo.exceptions import ValidationError + + +class EstatePropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "Real Estate Property Offer" + _order = "price desc" + + price = fields.Float() + status = fields.Selection( + selection=[ + ('accepted', 'Accepted'), + ('refused', 'Refused'), + ], + copy=False, + ) + partner_id = fields.Many2one( + "res.partner", + string="Partner", + required=True, + ) + property_id = fields.Many2one( + "estate.property", + string="Property", + required=True, + ) + property_type_id = fields.Many2one( + related="property_id.type_id", + string="Property Type", + store=True, + ) + validity = fields.Integer(default=7) + date_deadline = fields.Date( + compute="_compute_date_deadline", + inverse="_inverse_date_deadline", + ) + + @api.depends("validity", "create_date") + def _compute_date_deadline(self): + for record in self: + start_date = record.create_date.date() if record.create_date else fields.Date.today() + record.date_deadline = fields.Date.add( + start_date, + days=record.validity + ) + + def _inverse_date_deadline(self): + for record in self: + record.validity = ( + record.date_deadline - (record.create_date.date() or fields.Date.today()) + ).days + + def action_accept_offer(self): + for record in self: + if record.property_id.state == 'sold': + raise exceptions.UserError("Property is already sold.") + if record.property_id.garden and record.property_id.garden_orientation == 'south': + if record.price < record.property_id.expected_price: + raise ValidationError("South facing house should have offer with >= tothe prop expected price.") + record.property_id.write({ + "state": "offer_accepted", + "selling_price": record.price, + "buyer": record.partner_id, + }) + record.write({ + "status": "accepted", + }) + + def action_refuse_offer(self): + for record in self: + record.status = 'refused' + + @api.model + def create(self, vals_list): + for vals in vals_list: + prop = self.env['estate.property'].browse(vals.get('property_id')) + + if prop.best_price > 0 and vals.get('price') < prop.best_price: + raise exceptions.UserError( + f"The offer must be at least {prop.best_price}." + ) + if prop.state == 'new': + prop.state = 'offer_received' + + return super().create(vals_list) + + _check_price = models.Constraint( + 'CHECK(price > 0)', + 'The offer price should be > 0', + ) diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..38a8c58a552 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,15 @@ +from odoo import fields, models + + +class EstatePropertyTag(models.Model): + _name = "estate.property.tag" + _description = "Real Estate Property Tag" + _order = "name" + + name = fields.Char(required=True) + color = fields.Integer() + + _check_name_unique = models.Constraint( + 'UNIQUE(name)', + 'The property 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..adaee8de05f --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,18 @@ +from odoo import api, fields, models + + +class EstatePropertyType(models.Model): + _name = "estate.property.type" + _description = "Real Estate Property Type" + _order = "sequence, name" + + name = fields.Char(required=True) + property_ids = fields.One2many('estate.property', 'type_id') + sequence = fields.Integer('Sequence', default=1, help="") + offer_ids = fields.One2many('estate.property.offer', 'property_type_id') + offer_count = fields.Integer(compute='_compute_offer_count') + + @api.depends("offer_ids") + def _compute_offer_count(self): + for record in self: + record.offer_count = len(record.offer_ids) diff --git a/estate/models/res_user.py b/estate/models/res_user.py new file mode 100644 index 00000000000..76f4160796e --- /dev/null +++ b/estate/models/res_user.py @@ -0,0 +1,11 @@ +from odoo import fields, models + + +class ResUsers(models.Model): + _inherit = "res.users" + + property_ids = fields.One2many( + "estate.property", + "salesperson_id", + domain=[("state", "in", ["new", "offer_received"])] + ) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..2f94744b9ef --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,5 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1 +access_estate_property_type,access_estate_property_type,model_estate_property_type,base.group_user,1,1,1,1 +access_estate_property_tag,access_estate_property_tag,model_estate_property_tag,base.group_user,1,1,1,1 +access_estate_property_offer,access_estate_property_offer,model_estate_property_offer,base.group_user,1,1,1,1 diff --git a/estate/tests/__init__.py b/estate/tests/__init__.py new file mode 100644 index 00000000000..576617cccff --- /dev/null +++ b/estate/tests/__init__.py @@ -0,0 +1 @@ +from . import test_estate_property diff --git a/estate/tests/test_estate_property.py b/estate/tests/test_estate_property.py new file mode 100644 index 00000000000..5df181fb0a0 --- /dev/null +++ b/estate/tests/test_estate_property.py @@ -0,0 +1,44 @@ +from odoo.exceptions import ValidationError +from odoo.tests import TransactionCase +from odoo import Command + + +class TestEstateProperty(TransactionCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.estate = cls.env['estate.property'].create({ + 'name': 'Super test estate', + 'expected_price': 100000.0, + 'state': 'new', + }) + cls.test_partner = cls.env['res.partner'].create({ + 'name': 'Maman ours', + }) + + def test_estate_best_price(self): + ''' + Ensure best price is correctly updated when an offer is received. + ''' + self.assertEqual(self.estate.best_price, 0.0) + self.estate.offer_ids = [Command.create({ + 'price': 125000.0, + 'partner_id': self.test_partner.id, + })] + self.assertEqual(self.estate.best_price, 125000.0) + + def test_accept_offer_south_facing_garden(self): + ''' + Ensure offers for estates with south-facing gardens can only be accepted if above expected + price. + ''' + self.estate.garden = True + self.estate.garden_orientation = 'south' + self.estate.expected_price = 500000 + self.estate.offer_ids = [Command.create({ + 'price': 475000.0, + 'partner_id': self.test_partner.id, + })] + with self.assertRaises(ValidationError): + self.estate.offer_ids.action_accept_offer() diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..58eac830bb6 --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..c8c5dc1697a --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,43 @@ + + + Property Offers + estate.property.offer + list,form + [('property_type_id', '=', active_id)] + + + + 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..fe6c10c8fd4 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,132 @@ + + + Properties + estate.property + list,form,kanban + {'search_default_available': True} + + + + estate.property.list + estate.property + + + + + + + + + + + + + + + + estate.property.form + estate.property + +
+
+
+ +
+

+ +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + estate.property.search + estate.property + + + + + + + + + + + + + + + + + estate.property.kanban + estate.property + + + + + +
+ +
+ Expected Price: +
+
+ Best Price: +
+
+ Selling Price: +
+ +
+
+
+
+
+
+
diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml new file mode 100644 index 00000000000..a55e656abc9 --- /dev/null +++ b/estate/views/res_users_views.xml @@ -0,0 +1,16 @@ + + + + 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..cfc9f1060cc --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,13 @@ +{ + 'name': "Estate Account", + 'version': '19.0.0.1.0', + 'depends': [ + 'estate', + 'account' + ], + 'category': 'Tutorials', + 'application': False, + 'data': [], + 'author': "Odoo", + 'license': 'AGPL-3' +} diff --git a/estate_account/models/__init__.py b/estate_account/models/__init__.py new file mode 100644 index 00000000000..5e1963c9d2f --- /dev/null +++ b/estate_account/models/__init__.py @@ -0,0 +1 @@ +from . import estate_property diff --git a/estate_account/models/estate_property.py b/estate_account/models/estate_property.py new file mode 100644 index 00000000000..afbf8a20263 --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,27 @@ +from odoo import Command, models + + +class EstateProperty(models.Model): + _inherit = "estate.property" + + def action_sold(self): + for record in self: + self.env["account.move"].create( + { + "partner_id": record.buyer_id.id, + "move_type": "out_invoice", + "invoice_line_ids": [ + Command.create({ + "name": f"Commission for {record.name}", + "quantity": 1.0, + "price_unit": record.selling_price * 0.06, + }), + Command.create({ + "name": "Administrative Fees", + "quantity": 1.0, + "price_unit": 100.0, + }), + ], + } + ) + return super().action_sold() diff --git a/k b/k new file mode 100644 index 00000000000..ebce8f24bfe --- /dev/null +++ b/k @@ -0,0 +1,6 @@ + id | create_uid | write_uid | create_date | write_date | bedrooms | living_area | facades | garden_area | name | postcode | garden_orientation | date_availability | description | garage | garden | expected_price | selling_price | active | state +----+------------+-----------+----------------------------+----------------------------+----------+-------------+---------+-------------+-------+----------+--------------------+-------------------+-------------+--------+--------+----------------+---------------+--------+------- + 4 | 2 | 2 | 2026-04-22 15:27:35.807356 | 2026-04-22 15:27:35.807356 | 2 | 0 | 0 | 0 | sdgsg | | | 2026-07-22 | | f | f | 0 | | t | new + 3 | 2 | 2 | 2026-04-22 14:59:56.16026 | 2026-04-23 10:00:36.637113 | 2 | 0 | 0 | 0 | asf | 1244124 | | 2026-07-22 | | t | t | 22222 | | t | new +(2 rows) + diff --git a/website_airproof/__manifest__.py b/website_airproof/__manifest__.py index f6cd9dc0d5e..2c2c62d6a18 100644 --- a/website_airproof/__manifest__.py +++ b/website_airproof/__manifest__.py @@ -7,6 +7,9 @@ 'license': 'LGPL-3', 'depends': ['website_sale', 'website_sale_wishlist', 'website_blog', 'website_mass_mailing'], 'data': [ + # Snippets + 'views/snippets/options.xml', + 'views/snippets/s_airproof_carousel.xml', # Options 'data/presets.xml', 'data/website.xml', @@ -24,9 +27,6 @@ 'views/website_templates.xml', 'views/website_sale_templates.xml', 'views/website_sale_wishlist_templates.xml', - # Snippets - 'views/snippets/options.xml', - 'views/snippets/s_airproof_carousel.xml', # Images 'data/images.xml', ],