diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js new file mode 100644 index 00000000000..bfcc7313dca --- /dev/null +++ b/awesome_owl/static/src/card/card.js @@ -0,0 +1,11 @@ +/** @odoo-module */ + +import { Component } from "@odoo/owl"; + +export class Card extends Component { + static template = "awesome_owl.Card"; + static props = { + title: String, + content: String, + }; +} diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml new file mode 100644 index 00000000000..23878b0723a --- /dev/null +++ b/awesome_owl/static/src/card/card.xml @@ -0,0 +1,11 @@ + + + +
+
+
+

+
+
+
+
diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js new file mode 100644 index 00000000000..e7484e880d9 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.js @@ -0,0 +1,21 @@ +/** @odoo-module */ + +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: 1 }); + } + + increment() { + this.state.value = this.state.value + 1; + 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..4791fb6cd42 --- /dev/null +++ b/awesome_owl/static/src/counter/counter.xml @@ -0,0 +1,9 @@ + + + +
+ Counter: + +
+
+
diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 657fb8b07bb..7907653987d 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,7 +1,21 @@ /** @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_list/todo_list" export class Playground extends Component { static template = "awesome_owl.playground"; + static components = { Counter, Card, TodoList }; + + setup() { + this.str1 = "
some content
"; + this.str2 = markup("
some content
"); + this.sum = useState({ value: 2 }); + } + + incrementSum() { + this.sum.value++; + } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..9b5a175a0e5 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -4,6 +4,17 @@
hello world + + +
The sum is: +
+
+
+ + +
+
+
diff --git a/awesome_owl/static/src/todo_list/todo_item.js b/awesome_owl/static/src/todo_list/todo_item.js new file mode 100644 index 00000000000..164ced55c07 --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_item.js @@ -0,0 +1,13 @@ +/** @odoo-module */ + +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 } + } + }; +} diff --git a/awesome_owl/static/src/todo_list/todo_item.xml b/awesome_owl/static/src/todo_list/todo_item.xml new file mode 100644 index 00000000000..3ae8b5e883b --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_item.xml @@ -0,0 +1,9 @@ + + + +
+ . + +
+
+
diff --git a/awesome_owl/static/src/todo_list/todo_list.js b/awesome_owl/static/src/todo_list/todo_list.js new file mode 100644 index 00000000000..5fc367c8b2d --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_list.js @@ -0,0 +1,24 @@ +/** @odoo-module */ + +import { Component, useState } from "@odoo/owl"; +import { TodoItem } from "./todo_item"; + +export class TodoList extends Component { + static template = "awesome_owl.TodoList"; + static components = { TodoItem }; + + setup() { + this.todos = useState([]); + this.newId = 1; + } + addTodo(e) { + if (e.keyCode === 13 && e.target.value != "") { + this.todos.push({ + id: this.newId++, + description: e.target.value, + isCompleted: false + }) + e.target.value = "" + } + } +} diff --git a/awesome_owl/static/src/todo_list/todo_list.xml b/awesome_owl/static/src/todo_list/todo_list.xml new file mode 100644 index 00000000000..dccfc4bee98 --- /dev/null +++ b/awesome_owl/static/src/todo_list/todo_list.xml @@ -0,0 +1,13 @@ + + + +
+ +
+ + + +
+
+
+
diff --git a/estate/__init__.py b/estate/__init__.py new file mode 100644 index 00000000000..d6210b1285d --- /dev/null +++ b/estate/__init__.py @@ -0,0 +1,3 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import models diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 00000000000..976cfb98e5b --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,25 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'Real Estate', + 'description': 'A Module which covers all workflows related to real estate', + 'summary': 'Module to track all things related to real estate of any company', + 'version': '1.0', + 'category': 'Estate', + 'depends': [ + 'base' + ], + 'data': [ + 'security/ir.model.access.csv', + 'views/estate_property_views.xml', + 'views/estate_property_tag_views.xml', + 'views/estate_property_offer_views.xml', + 'views/estate_property_type_views.xml', + 'views/estate_property_menus.xml', + 'views/res_users_inherited_views.xml' + ], + 'license': 'LGPL-3', + 'installable': True, + 'application': True, + 'auto_install': False +} diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..f7e4cc6f3dd --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1,7 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import estate_property +from . import estate_property_type +from . import estate_property_tag +from . import estate_property_offer +from . import res_users diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..0554b1a041c --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,160 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import datetime, timedelta +from odoo import api, models, fields +from odoo.exceptions import UserError, ValidationError + + +class EstateProperty(models.Model): + _name = 'estate.property' + _description = 'Estate Property' + _order = 'id desc' + + name = fields.Char(string='Estate Property Name', required=True) + description = fields.Text(string='Estate Property Description') + postcode = fields.Char(string='Estate Property Postcode') + date_availability = fields.Date(string='Estate Property Date Availability', copy=False, default=lambda self: datetime.now() + timedelta(days=90)) + expected_price = fields.Float(string='Expected Price Of Property', required=True) + selling_price = fields.Float(string='Selling Price of Property', readonly=True, copy=False) + bedrooms = fields.Integer(string='Number of Bedrooms in Property', default=2) + living_area = fields.Integer(string='Number of Living Room in Property') + facades = fields.Integer(string='Number of Facades in Property') + garage = fields.Boolean(string='Property have garage or not') + garden = fields.Boolean(string='Property have Garden or not') + garden_area = fields.Integer(string='Number of Garden Area') + garden_orientation = fields.Selection( + string='Orientation of Garden', + selection=[ + ('north', 'North'), + ('south', 'South'), + ('east', 'East'), + ('west', 'West') + ], + help='Different Types of Directions') + active = fields.Boolean(default=True) + state = fields.Selection( + string='State', + default='new', + copy=False, + required=True, + selection=[ + ('new', 'New'), + ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('cancel', 'Cancelled') + ], + help='State of the property') + property_type_id = fields.Many2one('estate.property.type', string='Property Type Id') + buyer_id = fields.Many2one('res.partner', string='Buyer', copy=False) + salesman_id = fields.Many2one('res.users', string='Salesman', default=lambda self: self.env.user) + tag_ids = fields.Many2many('estate.property.tag', string='Tags') + offer_ids = fields.One2many('estate.property.offer', 'property_id', string='offer') + total_area = fields.Float(compute='_compute_total_area') + best_price = fields.Float(compute='_compute_best_price', string='Best Offer Price') + + _sql_constraints = [ + ('expected_price', 'CHECK(expected_price > 0)', 'Expected price of the property must be positive'), + ('selling_price', 'CHECK(selling_price >= 0)', 'Selling price of the property must be positive'), + ] + + @api.depends('living_area', 'garden_area') + def _compute_total_area(self): + """ + This will compute total area whenever living area and garden area is changed + and count total area in for the record + """ + + for record in self: + record.total_area = record.garden_area + record.living_area + + @api.depends('offer_ids.price') + def _compute_best_price(self): + """ + This will compute best offer price among all offers + """ + + for record in self: + record.best_price = max(record.offer_ids.mapped('price'), default=0.0) + + @api.onchange('garden') + def _onchange_garden(self): + """ + Handles changes to the `garden` field. + - If `garden` is checked (True), sets default values: + - `garden_area` to 10 + - `garden_orientation` to `north` + - If `garden` is unchecked (False), clears both fields: + - `garden_area` and `garden_orientation` are set to False + This method is intended to be used as an onchange handler in Odoo views. + """ + + for record in self: + if record.garden: + record.garden_area = 10 + record.garden_orientation = 'north' + else: + record.garden_area = False + record.garden_orientation = False + + def property_sold_action(self): + """ + Sold action for property + + Raises: + UserError: If the property already sold + + Returns: + bool: True if operation is successful. + """ + + for record in self: + if record.state == 'cancel': + raise UserError('Cancelled property can not be sold') + else: + record.state = 'sold' + return True + + def property_cancel_action(self): + """ + Cancel action for property + + Raises: + UserError: If the property already cancelled + + Returns: + bool: True if operation is successful. + """ + + for record in self: + if record.state == 'sold': + raise UserError('Sold property can not be cancelled') + else: + record.state = 'cancel' + return True + + @api.constrains('selling_price') + def check_offer_price(self): + """ + This python constraint will check that selling price of the property + should be greater than 90 % of expected price of the property + + Raises: + ValidationError: If selling price of property is less than + 90% of expected price of the property + """ + for record in self: + if record.selling_price and record.selling_price < (0.9 * record.expected_price): + raise ValidationError(f'Selling price ({record.selling_price}) should be greater than 90% ({round(0.9 * record.expected_price, 2)}) of the expected price ({record.expected_price})') + + @api.ondelete(at_uninstall=False) + def _unlink_if_user_state(self): + """ + This validation will be checked for deletion of the property and + it will be deleted if property state should not be in `new` or `cancel` + + Raises: + UserError: If property is in `offer_received` or `offer_accepted` or `sold` state. + """ + if any(user.state not in ['new', 'cancel'] for user in self): + raise UserError('You can\'t delete property which is in offer received or offer accepted or sold state.') diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..b4364c3dc77 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,131 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import timedelta +from odoo import api, fields, models +from odoo.exceptions import UserError + + +class EstatePropertyOffer(models.Model): + _name = 'estate.property.offer' + _description = 'Estate Property Offer' + _order = 'price desc' + + price = fields.Float(string='Offer Price') + status = fields.Selection( + string='Status', + copy=False, + selection=[ + ('accepted', 'Accepted'), + ('refused', 'Refused') + ]) + partner_id = fields.Many2one('res.partner', required=True) + property_id = fields.Many2one('estate.property', required=True) + created_date = fields.Date(default=fields.Date.context_today, string='Created Date') + validity = fields.Integer(default=7, string='Validity (Days)') + date_deadline = fields.Date(compute='_compute_date_deadline', inverse='_inverse_date_deadline', string='Deadline Date', store='True') + property_type_id = fields.Many2one(related='property_id.property_type_id', store=True) + + _sql_constraints = [ + ('price', 'CHECK(price >= 0)', 'Offer price must be strictly positive') + ] + + @api.depends('created_date', 'validity') + def _compute_date_deadline(self): + """ + Computes the deadline date for each offer record based on its creation date and validity period. + + If both 'created_date' and 'validity' are set, 'date_deadline' is calculated by adding the validity (in days) + to the creation date. Otherwise, 'date_deadline' is set to False. + + This method is triggered automatically when either 'created_date' or 'validity' fields are changed. + """ + + for record in self: + if record.created_date and record.validity: + record.date_deadline = record.created_date + timedelta(days=record.validity) + else: + record.date_deadline = False + + def _inverse_date_deadline(self): + """ + Inverse method for computing the 'validity' field based on 'date_deadline' and 'created_date'. + + This method updates the 'validity' field for each record by calculating the number of days + between 'date_deadline' and 'created_date'. If either field is missing, 'validity' is set to 0. + + Used as an inverse function in computed fields to allow updating 'validity' when 'date_deadline' changes. + """ + + for record in self: + if record.date_deadline and record.created_date: + record.validity = (record.date_deadline - record.created_date).days + else: + record.validity = 0 + + def offer_accepted_action(self): + """ + Accepts an offer for a property, ensuring only one accepted offer per property. + + Iterates through each offer record, checks if there is already an accepted offer for the same property. + If an accepted offer exists, raises a UserError to prevent multiple accepted offers. + Otherwise, updates the property's selling price, buyer, and state to reflect the accepted offer, + and sets the offer's status to 'accepted'. + + Raises: + UserError: If the property already has an accepted offer. + + Returns: + bool: True if the operation is successful. + """ + + for record in self: + # breakpoint() + existing_offer = self.search([('property_id', '=', record.property_id.id), ('status', '=', 'accepted')]) + if existing_offer: + raise UserError('This property already has an accepted offer.') + property_record = record.property_id + property_record.selling_price = record.price + property_record.buyer_id = record.partner_id + record.status = 'accepted' + property_record.state = 'offer_accepted' + return True + + def offer_refused_action(self): + """ + This action used to refuse an offer. + + Returns: + bool: True when operation is successful. + """ + for record in self: + record.status = 'refused' + return True + + @api.model_create_multi + def create(self, vals): + """ + Creates new estate property offers and updates the related property's state and best price validation. + + For each offer in `vals`: + - If the related property's state is `new`, it is updated to `offer_received`. + - If the offer price is less than or equal to the property's best price, a UserError is raised. + + Args: + vals (list of dict): List of values for new estate property offers. + + Returns: + recordset: The newly created estate property offer records. + + Raises: + UserError: If the offer price is not greater than the property's best price. + """ + for record in vals: + property_id = record['property_id'] + if property_id: + property = self.env['estate.property'].browse(property_id) + if property.state == 'new': + property.state = 'offer_received' + if property and property.best_price: + if record['price'] <= property.best_price: + raise UserError('Offer price must be greater than best Offer Price') + return super().create(vals) diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..556ac2d20d7 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,16 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models, fields + + +class EstatePropertyTag(models.Model): + _name = 'estate.property.tag' + _description = 'Estate Property Tag' + _order = 'name' + + name = fields.Char(string='Estate Property Tag', required=True) + color = fields.Integer(string='Color', default=0) + + _sql_constraints = [ + ('name', 'UNIQUE(name)', 'Tag name should unique') + ] diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..bf107b744af --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,24 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, models, fields + + +class EstatePropertyType(models.Model): + _name = 'estate.property.type' + _description = 'Estate Property Type' + _order = 'sequence, name' + + name = fields.Char(string='Estate Property Type', required=True) + property_ids = fields.One2many('estate.property', 'property_type_id') + sequence = fields.Integer('Sequence', default=1, help='Used to order stages. Lower is better.') + offer_ids = fields.One2many('estate.property.offer', 'property_type_id', string='offer') + offer_count = fields.Char(string='Total numbers of offers', compute='_compute_offer_count') + + @api.depends('offer_ids') + def _compute_offer_count(self): + """ + Counts total number of offers which is offered to a property + """ + + for record in self: + record.offer_count = len(record.offer_ids) diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..e820eaea194 --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,13 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class ResUsers(models.Model): + _inherit = 'res.users' + + property_ids = fields.One2many( + comodel_name='estate.property', + inverse_name='salesman_id', + string='Property Associated with Users', + domain=[('state', '!=', 'sold'), ('state', '!=', 'cancel')]) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..210d51bb44c --- /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" +access_estate_property,access_estate_property,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 \ No newline at end of file diff --git a/estate/views/estate_property_menus.xml b/estate/views/estate_property_menus.xml new file mode 100644 index 00000000000..48c3a7d9536 --- /dev/null +++ b/estate/views/estate_property_menus.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..566c7b59832 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,55 @@ + + + + + + Estate Property Offers + estate.property.offer + list,form + [('property_type_id', '=', active_id)] + +

No Offers are found!

Add the offers + first.

+
+
+ + + + estate.property.offer.view.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..ff8a7e0ea7e --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,148 @@ + + + + + + Estate Properties + estate.property + list,form,kanban + {'search_default_state': True, 'search_default_current': True} + + + + + estate.property.view.list + estate.property + + + + + + + + + + + + + + + + + + + estate.property.view.form + estate.property + +
+
+
+ +
+

+ +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + + estate_property.view.kanban + estate.property + + + + + + + + +
Expected Price: +
+
+ Best Price: +
+
+ Selling Price: +
+
+ +
+
+
+
+
+
+ + + + estate.property.search + estate.property + + + + + + + + + + + + + + + +
diff --git a/estate/views/res_users_inherited_views.xml b/estate/views/res_users_inherited_views.xml new file mode 100644 index 00000000000..440a3535f50 --- /dev/null +++ b/estate/views/res_users_inherited_views.xml @@ -0,0 +1,18 @@ + + + + + + res.users.inherited.form.inherit + res.users + + + + + + + + + + + diff --git a/estate_account/__init__.py b/estate_account/__init__.py new file mode 100644 index 00000000000..d6210b1285d --- /dev/null +++ b/estate_account/__init__.py @@ -0,0 +1,3 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import models diff --git a/estate_account/__manifest__.py b/estate_account/__manifest__.py new file mode 100644 index 00000000000..5908a23e67d --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,21 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + + +{ + 'name': "Estate Account", + 'description': 'Real Estate Account Module which will used to help users regarding real estate properties', + 'version': '1.0', + 'category': 'Category', + 'depends': [ + 'base', + 'estate', + 'account' + ], + 'author': "Sanket Tank", + # data files always loaded at installation + 'data': [], + 'license': 'LGPL-3', + 'installable': True, + 'application': True, + 'auto_install': False +} diff --git a/estate_account/models/__init__.py b/estate_account/models/__init__.py new file mode 100644 index 00000000000..09b94f90f8d --- /dev/null +++ b/estate_account/models/__init__.py @@ -0,0 +1,3 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +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..ec38122fabb --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,26 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models, Command + + +class EstateProperty(models.Model): + _inherit = 'estate.property' + + def property_sold_action(self): + self.env['account.move'].sudo().create({ + 'partner_id': self.buyer_id.id, + 'move_type': 'out_invoice', + 'invoice_line_ids': [ + Command.create({ + 'name': '6% of the selling price', + 'quantity': 1, + 'price_unit': round(self.selling_price * 0.06, 2) + }), + Command.create({ + 'name': 'an additional 100.00 from administrative fees', + 'quantity': 1, + 'price_unit': '100.00' + }) + ] + }) + return super().property_sold_action()