From f4d667b8a430e9f31803dc453baeddcd8fe32b5b Mon Sep 17 00:00:00 2001 From: "Martin (mlef)" Date: Thu, 24 Jul 2025 11:04:30 +0200 Subject: [PATCH 1/2] [ADD] estate: Module for managing real esate Nullam eu ante vel est convallis dignissim. Fusce suscipit, wisi nec facilisis facilisis, est dui fermentum leo, quis tempor ligula erat quis odio. Nunc porta vulputate tellus. Nunc rutrum turpis sed pede. Sed bibendum. Aliquam posuere. Nunc aliquet, augue nec adipiscing interdum, lacus tellus malesuada massa, quis varius mi purus non odio. Pellentesque condimentum, magna ut suscipit hendrerit, ipsum augue ornare nulla, non luctus diam neque sit amet urna. Curabitur vulputate vestibulum lorem. Fusce sagittis, libero non molestie mollis, magna orci ultrices dolor, at vulputate neque nulla lacinia eros. Sed id ligula quis est convallis tempor. Curabitur lacinia pulvinar nibh. Nam a sapien. task-123 --- estate/Pipfile | 12 +++ estate/Pipfile.lock | 90 +++++++++++++++++++++ estate/__init__.py | 3 + estate/__manifest__.py | 18 +++++ estate/models/__init__.py | 5 ++ estate/models/estate_property.py | 58 +++++++++++++ estate/models/estate_property_tag.py | 11 +++ estate/models/estate_property_type.py | 11 +++ estate/security/ir.model.access.csv | 4 + estate/views/estate_menus.xml | 14 ++++ estate/views/estate_property_tag_views.xml | 17 ++++ estate/views/estate_property_type_views.xml | 17 ++++ estate/views/estate_property_views.xml | 86 ++++++++++++++++++++ 13 files changed, 346 insertions(+) create mode 100644 estate/Pipfile create mode 100644 estate/Pipfile.lock 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_tag.py create mode 100644 estate/models/estate_property_type.py create mode 100644 estate/security/ir.model.access.csv create mode 100644 estate/views/estate_menus.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 diff --git a/estate/Pipfile b/estate/Pipfile new file mode 100644 index 00000000000..621139aec7b --- /dev/null +++ b/estate/Pipfile @@ -0,0 +1,12 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +black = "*" + +[dev-packages] + +[requires] +python_version = "3.12" diff --git a/estate/Pipfile.lock b/estate/Pipfile.lock new file mode 100644 index 00000000000..01a1f81007f --- /dev/null +++ b/estate/Pipfile.lock @@ -0,0 +1,90 @@ +{ + "_meta": { + "hash": { + "sha256": "77e9b8783573c4507534daed344ef7fe1082f3c2cd1ca44f60a2b07523cac541" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.12" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "black": { + "hashes": [ + "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", + "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7", + "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da", + "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2", + "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", + "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", + "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", + "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", + "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32", + "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", + "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", + "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299", + "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0", + "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", + "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0", + "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", + "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355", + "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096", + "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e", + "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9", + "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", + "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==25.1.0" + }, + "click": { + "hashes": [ + "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", + "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b" + ], + "markers": "python_version >= '3.10'", + "version": "==8.2.1" + }, + "mypy-extensions": { + "hashes": [ + "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", + "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558" + ], + "markers": "python_version >= '3.8'", + "version": "==1.1.0" + }, + "packaging": { + "hashes": [ + "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", + "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f" + ], + "markers": "python_version >= '3.8'", + "version": "==25.0" + }, + "pathspec": { + "hashes": [ + "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", + "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" + ], + "markers": "python_version >= '3.8'", + "version": "==0.12.1" + }, + "platformdirs": { + "hashes": [ + "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", + "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4" + ], + "markers": "python_version >= '3.9'", + "version": "==4.3.8" + } + }, + "develop": {} +} diff --git a/estate/__init__.py b/estate/__init__.py new file mode 100644 index 00000000000..ae71d0a0ac6 --- /dev/null +++ b/estate/__init__.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python3 + +from . import models diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 00000000000..8ed8e1aaed0 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 + +{ + "name": "Real Estate", + "version": "1.0", + "category": "Tutorials/RealEstate", + "summary": "This is a test", + "depends": ["base"], + "installable": True, + "application": True, + "data": [ + "views/estate_property_tag_views.xml", + "views/estate_property_type_views.xml", + "views/estate_property_views.xml", + "views/estate_menus.xml", + "security/ir.model.access.csv", + ], +} diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..d89d8e555ab --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python3 + +from . import estate_property +from . import estate_property_type +from . import estate_property_tag diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..f265952a59b --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 + +# pyright: reportUnknownVariableType=false +# pyright: reportUnknownMemberType=false +# pyright: reportUnknownLambdaType=false + +from typing import final +from odoo import models, fields + + +@final +class EstateProperty(models.Model): + _name = "estate.property" + _description = "Test Model Description here" + name = fields.Char(required=True) + description = fields.Text() + postcode = fields.Char() + date_availability = fields.Date( + "Available From", + copy=False, + default=lambda _: fields.Date.add(fields.Datetime.now(), 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("Living Area (sqm)") + facades = fields.Integer() + garage = fields.Boolean() + garden = fields.Boolean() + garden_area = fields.Integer("Garden Area (sqm)") + garden_orientation = fields.Selection( + [ + ("north", "North"), + ("south", "South"), + ("east", "East"), + ("west", "West"), + ] + ) + active = fields.Boolean(default=True) + state = fields.Selection( + [ + ("new", "New"), + ("offer", "Offer"), + ("received", "Received"), + ("offer_accepted", "Offer Accepted"), + ("sold", "Sold"), + ("cancelled", "Cancelled"), + ], + copy=False, + default="new", + string="State", + ) + property_type_id = fields.Many2one("estate.property.type", string="Property Type") + buyer_id = fields.Many2one("res.partner", string="Buyer", copy=False) + salesperson_id = fields.Many2one( + "res.users", string="Sales Person", default=lambda self: self.env.uid + ) diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..9c0d801430b --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 + +from odoo import models, fields +from typing import final + + +@final +class EstatePropertyType(models.Model): + _name = "estate.property.tag" + _description = "Tag for a property" + name = fields.Char(required=True) diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..3183a3776df --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 + +from odoo import models, fields +from typing import final + + +@final +class EstatePropertyType(models.Model): + _name = "estate.property.type" + _description = "Type of property" + name = fields.Char(required=True) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..5558d0fbc60 --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,4 @@ +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 +estate.access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,1 +estate.access_estate_property_tag,access_estate_property_tag,estate.model_estate_property_tag,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 00000000000..499c5e70473 --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml new file mode 100644 index 00000000000..64a97149a29 --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,17 @@ + + + + + Property Tags + estate.property.tag + + + estate.property.tag.view.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..2a2b63659e0 --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,17 @@ + + + + + Property Types + estate.property.type + + + estate.property.type.view.list + 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..cf35208f77c --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,86 @@ + + + + Properties + estate.property + + + estate.property.view.list + estate.property + + + + + + + + + + + + + + estate.property.view.form + estate.property + +
+ + +

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + estate.property.view.search + estate.property + + + + + + + + + + + + + + +
From 39f8d55e842a7021ec4e070a2aca5acab37cfa76 Mon Sep 17 00:00:00 2001 From: "Martin (mlef)" Date: Tue, 29 Jul 2025 14:14:40 +0200 Subject: [PATCH 2/2] [ADD] estate: Finished all the changes as required by the tutorial chapters This commit was made mainly to play with runbot, as asked by the last chapter. Link to the followed tutorials: https://www.odoo.com/documentation/18.0/developer/tutorials/server_framework_101.html --- estate/__init__.py | 2 - estate/__manifest__.py | 4 +- estate/models/__init__.py | 4 +- estate/models/estate_property.py | 101 ++++++++++++++++-- estate/models/estate_property_offer.py | 77 +++++++++++++ estate/models/estate_property_tag.py | 12 ++- estate/models/estate_property_type.py | 33 +++++- estate/models/res_users.py | 9 ++ estate/security/ir.model.access.csv | 1 + estate/views/estate_property_offer_views.xml | 40 +++++++ estate/views/estate_property_tag_views.xml | 2 +- estate/views/estate_property_type_views.xml | 25 ++++- estate/views/estate_property_views.xml | 60 +++++++++-- estate/views/res_users_views.xml | 12 +++ estate_account/__init__.py | 1 + estate_account/__manifest__.py | 13 +++ estate_account/models/__init__.py | 2 + estate_account/models/estate_account.py | 9 ++ estate_account/models/estate_property.py | 42 ++++++++ estate_account/security/ir.model.access.csv | 2 + estate_account/views/estate_account_menus.xml | 4 + 21 files changed, 426 insertions(+), 29 deletions(-) create mode 100644 estate/models/estate_property_offer.py create mode 100644 estate/models/res_users.py create mode 100644 estate/views/estate_property_offer_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_account.py create mode 100644 estate_account/models/estate_property.py create mode 100644 estate_account/security/ir.model.access.csv create mode 100644 estate_account/views/estate_account_menus.xml diff --git a/estate/__init__.py b/estate/__init__.py index ae71d0a0ac6..0650744f6bc 100644 --- a/estate/__init__.py +++ b/estate/__init__.py @@ -1,3 +1 @@ -#!/usr/bin/env python3 - from . import models diff --git a/estate/__manifest__.py b/estate/__manifest__.py index 8ed8e1aaed0..e1bfa81e81a 100644 --- a/estate/__manifest__.py +++ b/estate/__manifest__.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - { "name": "Real Estate", "version": "1.0", @@ -9,9 +7,11 @@ "installable": True, "application": True, "data": [ + "views/estate_property_offer_views.xml", "views/estate_property_tag_views.xml", "views/estate_property_type_views.xml", "views/estate_property_views.xml", + "views/res_users_views.xml", "views/estate_menus.xml", "security/ir.model.access.csv", ], diff --git a/estate/models/__init__.py b/estate/models/__init__.py index d89d8e555ab..9a2189b6382 100644 --- a/estate/models/__init__.py +++ b/estate/models/__init__.py @@ -1,5 +1,5 @@ -#!/usr/bin/env python3 - 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 index f265952a59b..85a8829de3b 100644 --- a/estate/models/estate_property.py +++ b/estate/models/estate_property.py @@ -1,17 +1,14 @@ #!/usr/bin/env python3 -# pyright: reportUnknownVariableType=false -# pyright: reportUnknownMemberType=false -# pyright: reportUnknownLambdaType=false - from typing import final -from odoo import models, fields - +from odoo import models, fields, api, exceptions, tools @final class EstateProperty(models.Model): _name = "estate.property" _description = "Test Model Description here" + _order = "id desc" + name = fields.Char(required=True) description = fields.Text() postcode = fields.Char() @@ -21,6 +18,20 @@ class EstateProperty(models.Model): default=lambda _: fields.Date.add(fields.Datetime.now(), months=3), ) + @api.ondelete(at_uninstall=False) + def _unlink_except_wrong_state(self) -> None: + record: EstateProperty + for record in self: + if record.state in ["new", "cancelled"]: + raise exceptions.UserError(f"Cannot delete a property of state {record.state}") + + + @api.model_create_multi + def create(self: "EstateProperty", vals_list: list[api.ValuesType]): + for val in vals_list: + val["state"] = "received" + return super().create(vals_list) + expected_price = fields.Float(required=True) selling_price = fields.Float(readonly=True, copy=False) bedrooms = fields.Integer(default=2) @@ -29,6 +40,8 @@ class EstateProperty(models.Model): garage = fields.Boolean() garden = fields.Boolean() garden_area = fields.Integer("Garden Area (sqm)") + res_users_id = fields.Many2one("res.users") + garden_orientation = fields.Selection( [ ("north", "North"), @@ -42,7 +55,7 @@ class EstateProperty(models.Model): [ ("new", "New"), ("offer", "Offer"), - ("received", "Received"), + ("received", "Offer Received"), ("offer_accepted", "Offer Accepted"), ("sold", "Sold"), ("cancelled", "Cancelled"), @@ -56,3 +69,77 @@ class EstateProperty(models.Model): salesperson_id = fields.Many2one( "res.users", string="Sales Person", default=lambda self: self.env.uid ) + tag_ids = fields.Many2many("estate.property.tag", string="Tags") + estate_property_offer_ids = fields.One2many( + "estate.property.offer", "estate_property_id" + ) + + total_area = fields.Integer(compute="_compute_total_area") + + @api.depends("living_area", "garden_area") + def _compute_total_area(self) -> None: + for record in self: + record.total_area = record.living_area + record.garden_area + + best_price = fields.Float(compute="_compute_best_price", string="Best Offer") + + @api.depends("estate_property_offer_ids.price", "estate_property_offer_ids.status") + def _compute_best_price(self) -> None: + record: EstatePropert + for record in self: + offers = record.estate_property_offer_ids + offers = offers.filtered(lambda o: o.status != "refused") + record.best_price = max(offers.mapped("price") or [0]) + + @api.onchange("garden") + def _onchange_garden(self) -> None: + self.garden_area = 10 if self.garden else 0 + self.garden_orientation = "north" if self.garden else "" + + def action_cancel_property(self) -> bool: + record: EstateProperty + for record in self: + if record.state == "sold": + raise exceptions.UserError("You can't cancel a sold property") + record.state = "cancelled" + return True + + def action_sell_property(self) -> bool: + record: EstateProperty + for record in self: + if record.state == "cancelled": + raise exceptions.UserError("You can't sell a cancelled property") + record.state = "sold" + return True + + _sql_constraints = [ + ( + "positive_expected_price", + "CHECK(expected_price > 0)", + "Expected price should be stictly positive", + ), + ( + "positive_selling_price", + "CHECK(selling_price >= 0)", + "Selling price should be positive", + ), + ] + + @api.constrains("selling_price", "expected_price", "state") + def _check_selling_price(self) -> None: + record: EstateProperty + for record in self: + if record.state != "offer_accepted": + continue + is_lower = ( + tools.float_compare( + record.selling_price, + record.expected_price * 0.9, + precision_digits=2, + ) + == -1 + ) + if is_lower: + raise exceptions.ValidationError( + "Selling price cannot be lower than 90% of the expected price." + ) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..79f9945ad6e --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,77 @@ +from odoo import models, fields, api, exceptions + + +class EstatePropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "" + _order = "price desc" + + price = fields.Float() + status = fields.Selection( + [ + ("accepted", "Accepted"), + ("refused", "Refused"), + ], + copy=False, + ) + partner_id = fields.Many2one("res.partner", required=True) + estate_property_id = fields.Many2one("estate.property", required=True) + + validity = fields.Integer(default=7) + date_deadline = fields.Date( + compute="_compute_date_deadline", inverse="_inverse_date_deadline" + ) + + @api.depends("create_date", "validity") + def _compute_date_deadline(self) -> None: + record: EstatePropertyOffer + for record in self: + create_date: fields.Datetime = record.create_date or fields.Datetime.now() + record.date_deadline = fields.Date.add(create_date, days=record.validity) + + def _inverse_date_deadline(self) -> None: + # FIXME wasted too much time on this. + record: EstatePropertyOffer + for record in self: + pass + + @api.model_create_multi + def create(self: 'EstatePropertyOffer', vals_list: list[api.ValuesType]): + for val in vals_list: + property_id: int = val['estate_property_id'] + property = self.env['estate.property'].browse(property_id) + prices = property.estate_property_offer_ids.mapped('price') + min_price = min(prices or [0]) + if val['price'] < min_price: + raise exceptions.UserError(f'Price cannot be lower than ${min_price}') + + + return super().create(vals_list) + + def action_accept_offer(self) -> bool: + record: EstatePropertyOffer + for record in self: + all_offers = record.estate_property_id.estate_property_offer_ids + if all_offers.filtered(lambda o: o.status == "accepted"): + raise exceptions.UserError("You can't accept two offers") + record.estate_property_id.buyer_id = record.partner_id + record.estate_property_id.selling_price = record.price + record.estate_property_id.state = "offer_accepted" + record.status = "accepted" + return True + + def action_refuse_offer(self) -> bool: + record: EstatePropertyOffer + for record in self: + record.status = "refused" + return True + + _sql_constraints: list[tuple[str, str, str]] = [ + ( + "offer_price_strictly_positive", + "CHECK(offer > 0)", + "Offer price must be stricly positive.", + ), + ] + + property_type_id = fields.Many2one(related="estate_property_id.property_type_id", store=True) diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py index 9c0d801430b..e6e16317e18 100644 --- a/estate/models/estate_property_tag.py +++ b/estate/models/estate_property_tag.py @@ -5,7 +5,17 @@ @final -class EstatePropertyType(models.Model): +class EstatePropertyTag(models.Model): _name = "estate.property.tag" _description = "Tag for a property" + _order = "name asc" + name = fields.Char(required=True) + + _sql_constraints: list[tuple[str, str, str]] = [ + ( + "unique_name", + "UNIQUE (name)", + "Tag name should be unique", + ), + ] diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py index 3183a3776df..c6ab0f6dff9 100644 --- a/estate/models/estate_property_type.py +++ b/estate/models/estate_property_type.py @@ -1,11 +1,34 @@ -#!/usr/bin/env python3 +from odoo import models, fields, api -from odoo import models, fields -from typing import final - -@final class EstatePropertyType(models.Model): _name = "estate.property.type" _description = "Type of property" + _order = "sequence, name, id" + name = fields.Char(required=True) + + sequence = fields.Integer( + "Sequence", default=1, help="Used to order stages. Lower is better." + ) + + _sql_constraints: list[tuple[str, str, str]] = [ + ( + "unique_name", + "UNIQUE (name)", + "Type name should be unique", + ), + ] + + property_ids = fields.One2many("estate.property", "property_type_id") + + 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) -> None: + record: EstatePropertyType + 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..16221b96e10 --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,9 @@ +from odoo import models, fields + +from odoo import models, fields + + +class ResUsers(models.Model): + _inherit = "res.users" + + property_ids = fields.One2many("estate.property", "res_users_id") diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv index 5558d0fbc60..ce69c72c947 100644 --- a/estate/security/ir.model.access.csv +++ b/estate/security/ir.model.access.csv @@ -1,4 +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 estate.access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,1 +estate.access_estate_property_offer,access_estate_property_offer,estate.model_estate_property_offer,base.group_user,1,1,1,1 estate.access_estate_property_tag,access_estate_property_tag,estate.model_estate_property_tag,base.group_user,1,1,1,1 diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..90fc4882543 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,40 @@ + + + + Offers + estate.property.offer + {'search_default_available': 1, 'create': False} + [('property_type_id', '=', active_id)] + + + estate.property.offer.view.list + estate.property.offer + + + + + + + + + + + + + + + + + diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml index cf35208f77c..215e1c7cefc 100644 --- a/estate/views/estate_property_views.xml +++ b/estate/views/estate_property_views.xml @@ -3,19 +3,48 @@ Properties estate.property + {'search_default_available': 1} + kanban,form + + + estate.property.view.kanban + estate.property + + + + + +
+ + + + + + + +
+
+
+
+
estate.property.view.list estate.property - + + - + - + + @@ -24,21 +53,30 @@ estate.property
+
+

+ + + - + + @@ -50,9 +88,14 @@ - - - + + + + + + + + @@ -75,8 +118,9 @@ - + + diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml new file mode 100644 index 00000000000..63a20edd667 --- /dev/null +++ b/estate/views/res_users_views.xml @@ -0,0 +1,12 @@ + + + + res.users.view.form + 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..e5b7cd9888e --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,13 @@ +{ + "name": "Estate Account", + "version": "1.0", + "category": "Tutorials/EstateAccount", + "summary": "Lorem Ipsum", + "depends": ["base", 'estate', 'account'], + "installable": True, + "application": True, + "data": [ + "views/estate_account_menus.xml", + "security/ir.model.access.csv", + ], +} diff --git a/estate_account/models/__init__.py b/estate_account/models/__init__.py new file mode 100644 index 00000000000..89fe4948834 --- /dev/null +++ b/estate_account/models/__init__.py @@ -0,0 +1,2 @@ +from . import estate_account +from . import estate_property diff --git a/estate_account/models/estate_account.py b/estate_account/models/estate_account.py new file mode 100644 index 00000000000..b57a6ee26c0 --- /dev/null +++ b/estate_account/models/estate_account.py @@ -0,0 +1,9 @@ +from odoo import models, fields + + +class EstateAccount(models.Model): + _name = "estate.account" + _description = "Account of real estate" + _order = "name asc" + + name = fields.Char(required=True) diff --git a/estate_account/models/estate_property.py b/estate_account/models/estate_property.py new file mode 100644 index 00000000000..e29bd60cee7 --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,42 @@ +from odoo import models, fields + +""" +NOTE +An account move is just a group of operations (account move lines) +E.g.: a vendor bill is an account move + a customer invoice is an account move too +""" + + +class EstateProperty(models.Model): + _inherit = "estate.property" + + def action_sell_property(self) -> bool: + if not super().action_sell_property(): + return False + record: EstateProperty + for record in self: + partner_id: int = record.buyer_id.id + invoice_lines = [ + fields.Command.create( + { + "name": "Partial pay", + "quantity": 1, + "price_unit": record.selling_price / 100 * 6 + } + ), + fields.Command.create( + { + "name": "Administrative fees", + "quantity": 1, + "price_unit": 100 + } + ), + ] + account_move = self.env["account.move"].create( + { + "move_type": "out_invoice", + "partner_id": partner_id, + "invoice_line_ids": invoice_lines + } + ) diff --git a/estate_account/security/ir.model.access.csv b/estate_account/security/ir.model.access.csv new file mode 100644 index 00000000000..5fa6dd6e810 --- /dev/null +++ b/estate_account/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +estate_account.access_estate_account,access_estate_account,estate_account.model_estate_account,base.group_user,1,1,1,1 diff --git a/estate_account/views/estate_account_menus.xml b/estate_account/views/estate_account_menus.xml new file mode 100644 index 00000000000..38f0b06fb9a --- /dev/null +++ b/estate_account/views/estate_account_menus.xml @@ -0,0 +1,4 @@ + + + +