From 4fd2d3bad64151e7171113ab3068b03bd68b806d Mon Sep 17 00:00:00 2001 From: "Julien Piron (jupir)" Date: Tue, 19 May 2026 11:53:26 +0200 Subject: [PATCH] [ADD] estate,estate_accountant: technical onboarding commit jupir's technical onboarding task-6229399 --- awesome_owl/static/src/playground.js | 10 +- awesome_owl/static/src/playground.xml | 5 +- estate/__init__.py | 1 + estate/__manifest__.py | 18 +++ estate/models/__init__.py | 5 + estate/models/estate_property.py | 107 +++++++++++++++ estate/models/estate_property_offer.py | 71 ++++++++++ estate/models/estate_property_tag.py | 12 ++ estate/models/estate_property_type.py | 29 ++++ estate/models/res_users.py | 11 ++ estate/security/ir.model.access.csv | 5 + estate/views/estate_property_menus.xml | 12 ++ estate/views/estate_property_offer_views.xml | 17 +++ estate/views/estate_property_tag_views.xml | 19 +++ estate/views/estate_property_type_views.xml | 49 +++++++ estate/views/estate_property_views.xml | 135 +++++++++++++++++++ estate/views/res_users_views.xml | 29 ++++ estate_account/__init__.py | 1 + estate_account/__manifest__.py | 9 ++ estate_account/models/__init__.py | 1 + estate_account/models/estate_property.py | 37 +++++ 21 files changed, 579 insertions(+), 4 deletions(-) create mode 100644 estate/__init__.py create mode 100644 estate/__manifest__.py create mode 100644 estate/models/__init__.py create mode 100644 estate/models/estate_property.py create mode 100644 estate/models/estate_property_offer.py create mode 100644 estate/models/estate_property_tag.py create mode 100644 estate/models/estate_property_type.py create mode 100644 estate/models/res_users.py create mode 100644 estate/security/ir.model.access.csv create mode 100644 estate/views/estate_property_menus.xml create mode 100644 estate/views/estate_property_offer_views.xml create mode 100644 estate/views/estate_property_tag_views.xml create mode 100644 estate/views/estate_property_type_views.xml create mode 100644 estate/views/estate_property_views.xml create mode 100644 estate/views/res_users_views.xml create mode 100644 estate_account/__init__.py create mode 100644 estate_account/__manifest__.py create mode 100644 estate_account/models/__init__.py create mode 100644 estate_account/models/estate_property.py diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js index 4ac769b0aa5..d6a478f5b94 100644 --- a/awesome_owl/static/src/playground.js +++ b/awesome_owl/static/src/playground.js @@ -1,5 +1,13 @@ -import { Component } from "@odoo/owl"; +import { Component, useState } from "@odoo/owl"; export class Playground extends Component { static template = "awesome_owl.playground"; + + setup() { + this.state = useState({ value: 0 }) + } + + increment() { + this.state.value++ + } } diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml index 4fb905d59f9..cf6e76f2a50 100644 --- a/awesome_owl/static/src/playground.xml +++ b/awesome_owl/static/src/playground.xml @@ -2,9 +2,8 @@ -
- hello world -
+

Counter:

+
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..30280b6d0bb --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,18 @@ +{ + "name": "Estate", + "summary": "Track your real estate business", + "depends": ["base"], + "data": [ + "security/ir.model.access.csv", + "views/estate_property_views.xml", + "views/estate_property_offer_views.xml", + "views/estate_property_tag_views.xml", + "views/estate_property_type_views.xml", + "views/res_users_views.xml", + "views/estate_property_menus.xml", + ], + "installable": True, + "application": True, + "author": "Julien (jupir)", + "license": "LGPL-3", +} diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..9c7b5068827 --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1,5 @@ +from . import estate_property +from . import estate_property_tag +from . import estate_property_type +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..9309da5296a --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,107 @@ +from dateutil.relativedelta import relativedelta +from datetime import datetime + +from odoo import api, fields, models +from odoo.exceptions import UserError, ValidationError +from odoo.tools.float_utils import float_compare, float_is_zero + + +class EstatePropery(models.Model): + _name = "estate.property" + _description = "Estate property" + _order = "id desc" + + name = fields.Char(required=True) + description = fields.Text() + postcode = fields.Char() + date_availability = fields.Date( + copy=False, default=datetime.today() + relativedelta(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="Orientation", + selection=[("N", "North"), ("E", "East"), ("S", "South"), ("W", "West")], + ) + total_area = fields.Integer(compute="_compute_total_area") + best_price = fields.Float(compute="_compute_best_price") + property_type_id = fields.Many2one("estate.property.type") + tag_ids = fields.Many2many("estate.property.tag") + salesperson_id = fields.Many2one("res.users", default=lambda self: self.env.user) + buyer_id = fields.Many2one("res.partner", copy=False) + offer_ids = fields.One2many("estate.property.offer", "property_id") + active = fields.Boolean(default=True) + state = fields.Selection( + selection=[ + ("new", "New"), + ("offer_received", "Offer Received"), + ("offer_accepted", "Offer Accepted"), + ("sold", "Sold"), + ("cancelled", "Cancelled"), + ], + copy=False, + default="new", + ) + + _expected_price_constraint = models.Constraint( + "CHECK(expected_price >= 0)", "Expected price must be positive." + ) + _selling_price_constraint = models.Constraint( + "CHECK(selling_price >= 0)", "Selling price must be positive." + ) + + @api.depends("living_area", "garden_area") + def _compute_total_area(self): + for property in self: + property.total_area = property.living_area + property.garden_area + + @api.depends("offer_ids.price") + def _compute_best_price(self): + for property in self: + property.best_price = ( + max(property.offer_ids.mapped("price")) if property.offer_ids else 0 + ) + + @api.constrains("selling_price", "expected_price") + def _check_selling_price_and_expected_price(self): + for property in self: + if ( + not float_is_zero(property.selling_price, 0) + and float_compare( + property.expected_price * 0.9, property.selling_price, 0 + ) + > 0 + ): + raise ValidationError( + "You cannot accept an offer lower than 90% of the expected price. Lower the expected price if you want to accept it." + ) + + @api.onchange("garden") + def _onchange_garden(self): + if self.garden: + self.garden_area = 10 + self.garden_orientation = "N" + else: + self.garden_area = None + self.garden_orientation = None + + @api.ondelete(at_uninstall=False) + def _unlink_except_valid_state(self): + if any(property.state not in ["new", "cancelled"] for property in self): + raise UserError("Can't delete a property if status is not New or Cancelled") + + def action_cancel_property(self): + if self.state == "sold": + raise UserError("Sold properties cannot be cancelled") + self.state = "cancelled" + + def action_mark_sold_property(self): + if self.state == "cancelled": + raise UserError("Cancelled properties cannot be sold") + self.state = "sold" diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..f30eceafb54 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,71 @@ +from datetime import timedelta +from dateutil.utils import today + +from odoo import api, fields, models +from odoo.exceptions import UserError +from odoo.tools.float_utils import float_compare + + +class EstatePropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "Offer for a property" + _order = "price desc" + + price = fields.Float() + status = fields.Selection( + selection=[("accepted", "Accepted"), ("refused", "Refused")], copy=False + ) + partner_id = fields.Many2one("res.partner", required=True) + property_id = fields.Many2one("estate.property", required=True) + property_type_id = fields.Many2one( + related="property_id.property_type_id", store=True + ) + validity = fields.Integer(default=7) + date_deadline = fields.Date( + compute="_compute_deadline", inverse="_inverse_deadline" + ) + + _check_price = models.Constraint("CHECK(price >= 0)", "Price must be positive") + + @api.depends("create_date", "validity") + def _compute_deadline(self): + for offer in self: + delta = timedelta(days=offer.validity) + offer.date_deadline = ( + offer.create_date.date() + delta + if offer.create_date + else today() + delta + ) + + def _inverse_deadline(self): + for offer in self: + offer.validity = ( + (offer.date_deadline - offer.create_date.date()).days + if offer.create_date + else (offer.date_line - today()).days + ) + + @api.model + def create(self, vals): + for record in vals: + property = self.env["estate.property"].browse(record["property_id"]) + + if float_compare(property.best_price, record["price"], 2) == 1: + raise UserError("You already have a higher offer") + + if hasattr(property, "state"): + property.state = "offer_received" + + return super().create(vals) + + def action_accept(self): + if self.property_id.state == "sold": + raise UserError("This property is already sold") + + self.status = "accepted" + self.property_id.state = "offer_accepted" + self.property_id.buyer_id = self.partner_id + self.property_id.selling_price = self.price + + def action_refuse(self): + self.status = "refused" diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..93eb7a0f3f1 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,12 @@ +from odoo import models, fields + + +class EstatePropertyTag(models.Model): + _name = "estate.property.tag" + _description = "Property tag" + _order = "name" + + name = fields.Char(required=True) + color = fields.Integer() + + _name_constraint = models.UniqueIndex("(name)", "Tag names 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..a8a561de30c --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,29 @@ +from odoo import api, models, fields, _ + + +class EstatePropertyType(models.Model): + _name = "estate.property.type" + _description = "Property type" + _order = "name" + + sequence = fields.Integer(default=1) + name = fields.Char(required=True) + property_ids = fields.One2many("estate.property", "property_type_id") + offer_ids = fields.One2many("estate.property.offer", "property_type_id", store=True) + offer_count = fields.Integer(compute="_compute_offer_count") + + @api.depends("property_ids", "offer_ids") + def _compute_offer_count(self): + for property_type in self: + property_type.offer_count = len(property_type.offer_ids) + + @api.readonly + def action_view_offers(self): + return { + "name": _("Offer()"), + "type": "ir.actions.act_window", + "res_model": "estate.property.offer", + "target": "current", + "view_mode": "list,form", + "domain": [("id", "in", self.property_ids.ids)], + } diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..c0601cdf7d9 --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,11 @@ +from odoo import fields, models + + +class EstateUser(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..9a113d0bcb2 --- /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_offer,access_estate_property_offer,model_estate_property_offer,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_type,access_estate_property_type,model_estate_property_type,base.group_user,1,1,1,1 diff --git a/estate/views/estate_property_menus.xml b/estate/views/estate_property_menus.xml new file mode 100644 index 00000000000..33ca260d0de --- /dev/null +++ b/estate/views/estate_property_menus.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..7926a79f9d4 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,17 @@ + + + + + estate.property.offer.list + estate.property.offer + + + + + + + + + + + diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml new file mode 100644 index 00000000000..e7a93a37aad --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,19 @@ + + + + + Property Tags + estate.property.tag + list,form + + + estate.property.tag.list + 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 00000000000..4a0fba9632d --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,49 @@ + + + + + Property Types + estate.property.type + list,form + + + estate.property.type.list + estate.property.type + + + + + + + + + estate.property.type.form + estate.property.type + +
+ +
+ +
+

+ +

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

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +