diff --git a/.gitignore b/.gitignore index b6e47617de..2f58826b2d 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,6 @@ dmypy.json # Pyre type checker .pyre/ + + +forme.txt \ No newline at end of file diff --git a/awesome_clicker/__init__.py b/awesome_clicker/__init__.py index 40a96afc6f..e69de29bb2 100644 --- a/awesome_clicker/__init__.py +++ b/awesome_clicker/__init__.py @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/awesome_clicker/__manifest__.py b/awesome_clicker/__manifest__.py index e57ef4d5bb..2073c8b2d1 100644 --- a/awesome_clicker/__manifest__.py +++ b/awesome_clicker/__manifest__.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- + { 'name': "Awesome Clicker", diff --git a/awesome_dashboard/__init__.py b/awesome_dashboard/__init__.py index b0f26a9a60..24dfd571b5 100644 --- a/awesome_dashboard/__init__.py +++ b/awesome_dashboard/__init__.py @@ -1,3 +1,3 @@ -# -*- coding: utf-8 -*- + from . import controllers diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py index 31406e8add..6861aa3f7c 100644 --- a/awesome_dashboard/__manifest__.py +++ b/awesome_dashboard/__manifest__.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- + { 'name': "Awesome Dashboard", diff --git a/awesome_dashboard/controllers/__init__.py b/awesome_dashboard/controllers/__init__.py index 457bae27e1..24dfd571b5 100644 --- a/awesome_dashboard/controllers/__init__.py +++ b/awesome_dashboard/controllers/__init__.py @@ -1,3 +1,3 @@ -# -*- coding: utf-8 -*- -from . import controllers \ No newline at end of file + +from . import controllers diff --git a/awesome_dashboard/controllers/controllers.py b/awesome_dashboard/controllers/controllers.py index 56d4a05128..90008c09dc 100644 --- a/awesome_dashboard/controllers/controllers.py +++ b/awesome_dashboard/controllers/controllers.py @@ -1,13 +1,13 @@ -# -*- coding: utf-8 -*- + import logging import random from odoo import http -from odoo.http import request logger = logging.getLogger(__name__) + class AwesomeDashboard(http.Controller): @http.route('/awesome_dashboard/statistics', type='json', auth='user') def get_statistics(self): @@ -33,4 +33,3 @@ def get_statistics(self): }, 'total_amount': random.randint(100, 1000) } - diff --git a/awesome_gallery/__init__.py b/awesome_gallery/__init__.py index a0fdc10fe1..a9e3372262 100644 --- a/awesome_gallery/__init__.py +++ b/awesome_gallery/__init__.py @@ -1,2 +1,2 @@ -# -*- coding: utf-8 -*- + from . import models diff --git a/awesome_gallery/__manifest__.py b/awesome_gallery/__manifest__.py index a51f4e5d1c..937a27f171 100644 --- a/awesome_gallery/__manifest__.py +++ b/awesome_gallery/__manifest__.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- + { 'name': "Gallery View", 'summary': """ diff --git a/awesome_gallery/models/__init__.py b/awesome_gallery/models/__init__.py index 7f0930ee74..fbe96de672 100644 --- a/awesome_gallery/models/__init__.py +++ b/awesome_gallery/models/__init__.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- + # import filename_python_file_within_folder_or_subfolder from . import ir_action from . import ir_ui_view diff --git a/awesome_gallery/models/ir_action.py b/awesome_gallery/models/ir_action.py index eae20acbf5..271875dd9d 100644 --- a/awesome_gallery/models/ir_action.py +++ b/awesome_gallery/models/ir_action.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- + from odoo import fields, models @@ -7,4 +7,4 @@ class ActWindowView(models.Model): view_mode = fields.Selection(selection_add=[ ('gallery', "Awesome Gallery") - ], ondelete={'gallery': 'cascade'}) + ], ondelete={'gallery': 'cascade'}) diff --git a/awesome_gallery/models/ir_ui_view.py b/awesome_gallery/models/ir_ui_view.py index 0c11b8298a..c6b454e432 100644 --- a/awesome_gallery/models/ir_ui_view.py +++ b/awesome_gallery/models/ir_ui_view.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- + from odoo import fields, models diff --git a/awesome_kanban/__init__.py b/awesome_kanban/__init__.py index 40a96afc6f..e69de29bb2 100644 --- a/awesome_kanban/__init__.py +++ b/awesome_kanban/__init__.py @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/awesome_kanban/__manifest__.py b/awesome_kanban/__manifest__.py index 752fc12f78..387848fa6f 100644 --- a/awesome_kanban/__manifest__.py +++ b/awesome_kanban/__manifest__.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- + { 'name': "Awesome Kanban", 'summary': """ diff --git a/awesome_owl/__init__.py b/awesome_owl/__init__.py index 457bae27e1..24dfd571b5 100644 --- a/awesome_owl/__init__.py +++ b/awesome_owl/__init__.py @@ -1,3 +1,3 @@ -# -*- coding: utf-8 -*- -from . import controllers \ No newline at end of file + +from . import controllers diff --git a/awesome_owl/__manifest__.py b/awesome_owl/__manifest__.py index 98c9595fb7..0b4c08415a 100644 --- a/awesome_owl/__manifest__.py +++ b/awesome_owl/__manifest__.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- + { 'name': "Awesome Owl", @@ -36,7 +36,7 @@ # required for fa icons 'web/static/src/libs/fontawesome/css/font-awesome.css', - + # include base files from framework ('include', 'web._assets_core'), diff --git a/awesome_owl/controllers/__init__.py b/awesome_owl/controllers/__init__.py index 457bae27e1..24dfd571b5 100644 --- a/awesome_owl/controllers/__init__.py +++ b/awesome_owl/controllers/__init__.py @@ -1,3 +1,3 @@ -# -*- coding: utf-8 -*- -from . import controllers \ No newline at end of file + +from . import controllers diff --git a/awesome_owl/controllers/controllers.py b/awesome_owl/controllers/controllers.py index bccfd6fe28..a50010bfca 100644 --- a/awesome_owl/controllers/controllers.py +++ b/awesome_owl/controllers/controllers.py @@ -1,5 +1,6 @@ from odoo import http -from odoo.http import request, route +from odoo.http import request + class OwlPlayground(http.Controller): @http.route(['/awesome_owl'], type='http', auth='public') diff --git a/estate/__init__.py b/estate/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /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 0000000000..a810c6bf4c --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,18 @@ +{ + 'name': 'Estate', + 'description': 'A module to manage real estate advertisments', + 'depends': ['base'], + + 'data': [ + 'security/ir.model.access.csv', + 'views/estate_property_views.xml', + 'views/estate_property_type_views.xml', + 'views/estate_property_tag_views.xml', + 'views/estate_menus.xml', + 'views/estate_property_offer_view.xml' + ], + + 'installable': True, + 'application': True, + 'license': 'AGPL-3', +} diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 0000000000..2f1821a39c --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1,4 @@ +from . import estate_property +from . import estate_property_type +from . import estate_property_tag +from . import estate_property_offer diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 0000000000..5982a68e79 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,102 @@ +from odoo import api, fields, models +from odoo.exceptions import UserError + + +class EstateProperty(models.Model): + _name = 'estate.property' + _description = 'The Real Estate Advertisement module' + _order = "id desc" + _sql_constraints = [ + ('check_expected_price', 'CHECK(expected_price > 0)', + 'A property expected price must be strictly positive'), + ('check_selling_price', 'CHECK(selling_price >= 0)', + 'A property selling price must be positive'), + ] + + name = fields.Char('Name', required=True) + description = fields.Text('Description') + postcode = fields.Char('Postcode') + bedrooms = fields.Integer('Bedrooms', default=2) + living_area = fields.Integer('Living Area (sqrm)') + facades = fields.Integer('Fcades') + garage = fields.Boolean(string="Garage", default=False) + garden = fields.Boolean(string="Garden", default=False) + garden_area = fields.Integer('Garden Area', default=0) + expected_price = fields.Float('Expected Price', required=True) + active = fields.Boolean(default=True) + sales_man_id = fields.Many2one("res.users", string="Salesmam", default=lambda self: self._uid) + buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False) + type_id = fields.Many2one("estate.property.type", string="Type") + tag_ids = fields.Many2many("estate.property.tag", string="Tags") + offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offer") + total_area = fields.Integer(compute="_compute_total_area", string="Total Area (sqrm)") + best_price = fields.Float(compute="_compute_best_price", string="Best Offer") + date_availability = fields.Date( + 'Availability Date', + copy=False, + default=fields.Datetime.add(fields.Datetime.today(), months=3), + ) + + selling_price = fields.Float( + 'Selling Price', + readonly=True, + copy=False, + ) + + garden_orientation = fields.Selection( + [ + ('north', 'North'), + ('south', 'South'), + ('east', 'East'), + ('west', 'West'), + ], + string='Garden Orientation', + ) + state = fields.Selection( + [ + ("new", "New"), + ("offer_received", "Offer Received"), + ("offer_accepted", "Offer Accepted"), + ("sold", "Sold"), + ("canceled", "Canceled"), + ], + default="new", + required=True, + copy=False, + string="Status", + ) + + @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") + def _compute_best_price(self): + for record in self: + record.best_price = max(record.offer_ids.mapped('price'), default=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_sold_property(self): + if "canceled" in self.mapped("state"): + raise UserError("Canceled properties cannot be sold.") + return self.write({"state": "sold", "active": False}) + + def action_cancel_property(self): + if "sold" in self.mapped("state"): + raise UserError("Sold properties cannot be canceled.") + return self.write({"state": "canceled", "active": False}) + + def unlink(self): + for record in self: + if not (record.state in ('new', 'canceled')): + raise UserError("It's not possible to delete a property which is not new or canceled!") + return super().unlink() diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 0000000000..aaca273817 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,75 @@ +from odoo import api, fields, models +from odoo.exceptions import UserError, ValidationError + + +class estatePropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "The offer of estate property" + _order = "price desc" + _sql_constraints = [ + ('check_offer_price', 'CHECK(price > 0)', + 'A property offer price must be strictly positive'), + ] + + price = fields.Float("Price") + partner_id = fields.Many2one("res.partner", required=True) + validity = fields.Integer(string="Validity (days)", default=7) + property_id = fields.Many2one('estate.property', required=True) + property_type_id = fields.Many2one("estate.property.type", related="property_id.type_id", store=True) + + state = fields.Selection( + selection=[ + ("accepted", "Accepted"), + ("refused", "Refused"), + ], + string="Status", + copy=False, + default=False, + ) + date_deadline = fields.Date( + string="Deadline (days)", + compute="_compute_date_deadline", + inverse="_inverse_date_deadline", + ) + + @api.depends('validity') + def _compute_date_deadline(self): + for record in self: + if record.create_date: + record.date_deadline = fields.Datetime.add(record.create_date, days=record.validity) + else: + record.date_deadline = fields.Datetime.add(fields.Datetime.today(), days=record.validity) + + def _inverse_date_deadline(self): + for offer in self: + date = offer.create_date.date() if offer.create_date else fields.Date.today() + offer.validity = (offer.date_deadline - date).days + + def action_accepted(self): + if "Accepted" in self.mapped("state"): + raise UserError("An offer as already been accepted.") + self.write( + { + "state": "accepted", + } + ) + return self.mapped("property_id").write( + { + "state": "offer_accepted", + "selling_price": self.price, + "buyer_id": self.partner_id.id, + } + ) + + def action_refused(self): + return self.write( + { + "state": "refused", + } + ) + + @api.constrains("price", "property_id.expected_price") + def check_price(self): + for record in self: + if record.price < (0.9 * record.property_id.expected_price): + raise ValidationError("The selling price must be at least 90%% of the expected price") diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 0000000000..696ab93314 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,14 @@ +from odoo import fields, models + + +class estatePropertyTag(models.Model): + _name = "estate.property.tag" + _description = "This module introduces property tags to the real estate model, allowing properties to be categorized with multiple descriptive tags like 'cozy' or 'renovated' using a many-to-many relationship." + _order = "name" + _sql_constraints = [ + ('check_tag', 'UNIQUE(name)', + 'A property tag name must be unique'), + ] + + name = fields.Char("Name", required=True) + color = fields.Integer("Color") diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 0000000000..eb88fad401 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,22 @@ +from odoo import api, fields, models + + +class estatePropertyType(models.Model): + _name = "estate.property.type" + _description = "the type of the property ..." + _order = "sequence, name" + _sql_constraints = [ + ('check_tag', 'UNIQUE(name)', + 'A property type name must be unique'), + ] + + name = fields.Char("Name") + property_ids = fields.One2many("estate.property", "type_id", string="Properties") + sequence = fields.Integer("Secuence", default=1) + offer_ids = fields.One2many("estate.property.offer", "property_type_id", string="offers") + offer_count = fields.Integer(string="Offers Count", compute="_compute_offers") + + @api.depends("offer_ids") + def _compute_offers(self): + for record in self: + record.offer_count = len(record.offer_ids) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 0000000000..08ba2fb083 --- /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_properry_model,access_model,model_estate_property,base.group_user,1,1,1,1 +estate_properry_type_model,access_model,model_estate_property_type,base.group_user,1,1,1,1 +estate_properry_tag_model,access_model,model_estate_property_tag,base.group_user,1,1,1,1 +estate_properry_offer_model,access_model,model_estate_property_offer,base.group_user,1,1,1,1 diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 0000000000..8b296912b0 --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/estate/views/estate_property_offer_view.xml b/estate/views/estate_property_offer_view.xml new file mode 100644 index 0000000000..b52343a6b9 --- /dev/null +++ b/estate/views/estate_property_offer_view.xml @@ -0,0 +1,40 @@ + + + + Offers + estate.property.offer + tree,form + [('property_type_id', '=', 'active_id')] + + + + estate.property.offer.form + estate.property.offer + +
+ + + + + + +
+
+
+ + + estate.property.offer.tree + estate.property.offer + + + + + + + + + + + + +
\ No newline at end of file diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml new file mode 100644 index 0000000000..d7ab432c23 --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,33 @@ + + + + Property Tags + estate.property.tag + tree,form + + + + estate.property.tag.tree + estate.property.tag + + + + + + + + + estate.property.tag.form + estate.property.tag + +
+ + + + + +
+
+
+ +
diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml new file mode 100644 index 0000000000..bdd7fd4e98 --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,64 @@ + + + + Property Types + estate.property.type + tree,form + + + + + estate.property.type.tree + estate.property.type + + + + + + + + + + estate.property.type.form + estate.property.type + +
+
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ + + estate.property.type.search + estate.property.type + + + + + + + +
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 0000000000..02aa9a1c9b --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,117 @@ + + + + property + estate.property + tree,form + {'search_default_available': 1} + + + + + estate_property_tree + estate.property + + + + + + + + + + + + + + + + estate_property_form + estate.property + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +