From f8d2cd909dcf12e2da35d3cde89952aaaf412743 Mon Sep 17 00:00:00 2001 From: "Pranjali Sangavekar(prsan)" Date: Mon, 22 Jun 2026 15:59:44 +0530 Subject: [PATCH 1/4] [ADD] estate_auction: implement auction workflow - Add auction sale mode for properties - Add auction state selection widget - Add Start Auction action and state transitions - Add auction details section to property form - Hide manual offer actions for auction properties - Validate auction offers against expected price --- estate/__init__.py | 1 + estate/__manifest__.py | 24 ++ estate/controllers/__init__.py | 1 + estate/controllers/main.py | 28 +++ estate/demo/estate_property_data.xml | 238 ++++++++++++++++++ estate/models/__init__.py | 5 + estate/models/estate_property.py | 119 +++++++++ estate/models/estate_property_offer.py | 83 ++++++ estate/models/estate_property_tag.py | 15 ++ estate/models/estate_property_type.py | 23 ++ estate/models/res_config_settings.py | 9 + estate/models/res_users.py | 12 + estate/security/ir.model.access.csv | 9 + estate/security/security.xml | 29 +++ estate/static/description/icon.png | Bin 0 -> 4777 bytes estate/views/estate_menus.xml | 12 + estate/views/estate_property_offers_views.xml | 60 +++++ estate/views/estate_property_tags_views.xml | 44 ++++ estate/views/estate_property_type_views.xml | 78 ++++++ estate/views/estate_property_views.xml | 173 +++++++++++++ estate/views/estate_res_users_view.xml | 29 +++ estate/views/res_config_settings.xml | 24 ++ estate/views/website_menu.xml | 9 + .../website_property_detail_template.xml | 75 ++++++ estate/views/website_property_template.xml | 39 +++ estate_account/__init__.py | 1 + estate_account/__manifest__.py | 8 + estate_account/models/__init__.py | 1 + estate_account/models/estate_property.py | 25 ++ estate_auction/__init__.py | 1 + estate_auction/__manifest__.py | 20 ++ estate_auction/models/__init__.py | 2 + estate_auction/models/estate_auction.py | 22 ++ .../models/estate_auction_offers.py | 32 +++ .../auction_state_selection.js | 55 ++++ .../auction_state_selection.scss | 20 ++ .../auction_state_selection.xml | 42 ++++ estate_auction/views/estate_auction_view.xml | 49 ++++ 38 files changed, 1417 insertions(+) create mode 100644 estate/__init__.py create mode 100644 estate/__manifest__.py create mode 100644 estate/controllers/__init__.py create mode 100644 estate/controllers/main.py create mode 100644 estate/demo/estate_property_data.xml 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_config_settings.py create mode 100644 estate/models/res_users.py create mode 100644 estate/security/ir.model.access.csv create mode 100644 estate/security/security.xml create mode 100644 estate/static/description/icon.png create mode 100644 estate/views/estate_menus.xml create mode 100644 estate/views/estate_property_offers_views.xml create mode 100644 estate/views/estate_property_tags_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/estate_res_users_view.xml create mode 100644 estate/views/res_config_settings.xml create mode 100644 estate/views/website_menu.xml create mode 100644 estate/views/website_property_detail_template.xml create mode 100644 estate/views/website_property_template.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 create mode 100644 estate_auction/__init__.py create mode 100644 estate_auction/__manifest__.py create mode 100644 estate_auction/models/__init__.py create mode 100644 estate_auction/models/estate_auction.py create mode 100644 estate_auction/models/estate_auction_offers.py create mode 100644 estate_auction/static/src/components/auction_state_selection/auction_state_selection.js create mode 100644 estate_auction/static/src/components/auction_state_selection/auction_state_selection.scss create mode 100644 estate_auction/static/src/components/auction_state_selection/auction_state_selection.xml create mode 100644 estate_auction/views/estate_auction_view.xml 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..a2b50536420 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,24 @@ +{ + 'name': 'RealEstate', + 'version': '1.0', + 'category': 'Real Estate/Brokerage', + 'summary': 'A module to manage real estate advertisements and property offers', + 'description': """A simple module to manage real estate ads.List your properties, track details like bedrooms and garden,let buyers make offers, and accept or reject them.""", + 'author': 'Pranjali Sangavekar(prsan)', + 'license': 'LGPL-3', + 'depends': ['base', 'mail'], + 'data': [ + 'security/security.xml', + 'security/ir.model.access.csv', + 'views/estate_property_views.xml', + 'views/estate_property_offers_views.xml', + 'views/estate_property_type_views.xml', + 'views/estate_property_tags_views.xml', + 'views/estate_res_users_view.xml', + 'views/estate_menus.xml', + ], + 'demo': [ + 'demo/estate_property_data.xml', + ], + 'application': True, +} diff --git a/estate/controllers/__init__.py b/estate/controllers/__init__.py new file mode 100644 index 00000000000..12a7e529b67 --- /dev/null +++ b/estate/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/estate/controllers/main.py b/estate/controllers/main.py new file mode 100644 index 00000000000..dc1ed5d7574 --- /dev/null +++ b/estate/controllers/main.py @@ -0,0 +1,28 @@ +from odoo import http +from odoo.http import request + + +class EstateController(http.Controller): + @http.route("/properties", auth="public", type="http", website=True) + def property_list(self, **kwargs): + properties = request.env["estate.property"].search([]) + return request.render( + "estate.property_list_template", + { + "properties": properties, + }, + ) + + @http.route( + "/properties/", auth="public", type="http", website=True + ) + def property_detail(self, property_id, **kwargs): + + property_record = request.env["estate.property"].browse(property_id) + + return request.render( + "estate.property_detail_template", + { + "property": property_record, + }, + ) diff --git a/estate/demo/estate_property_data.xml b/estate/demo/estate_property_data.xml new file mode 100644 index 00000000000..2c162652875 --- /dev/null +++ b/estate/demo/estate_property_data.xml @@ -0,0 +1,238 @@ + + + + Beautiful Villa in Downtown + A stunning 3-bedroom villa with modern amenities + 382421 + 2026-06-15 + 6767676.00 + 3 + 9234 + 2 + True + True + 5000 + south + new + + + + Cozy House with Garden + Perfect starter home with large garden + 400605 + 2026-07-01 + 100000.00 + 2 + 500 + 1 + False + True + 2000 + north + new + + + + Modern Apartment + Newly built apartment in city center + 382421 + 2026-05-20 + 250000.00 + 2 + 750 + 3 + True + False + 0 + east + new + + + + Traditional Cottage + Charming countryside cottage + 400605 + 2026-08-10 + 180000.00 + 3 + 650 + 2 + True + True + 3000 + west + new + + + + Luxury Penthouse + High-end penthouse with panoramic views + 382421 + 2026-04-01 + 500000.00 + 4 + 1200 + 2 + True + True + 1000 + south + new + + + + Seaside Bungalow + Relaxing bungalow with sea view + 600001 + 2026-10-01 + 420000.00 + 2 + 800 + 2 + False + True + 1500 + east + new + + + + Mountain Cabin + Cozy cabin in the mountains + 700002 + 2026-11-15 + 210000.00 + 3 + 600 + 1 + False + True + 1200 + west + new + + + + City Studio + Compact studio apartment for singles + 800003 + 2026-12-01 + 95000.00 + 1 + 350 + 1 + False + False + 0 + north + new + + + + Family Home + Spacious home perfect for families + 900004 + 2027-01-10 + 300000.00 + 4 + 1100 + 2 + True + True + 2000 + south + new + + + + Downtown Office Space + Modern office space in business district + 100005 + 2027-02-01 + 800000.00 + 0 + 2000 + 4 + True + False + 0 + east + new + + + + Suburban Ranch + Classic ranch house in quiet suburb + 200006 + 2027-03-15 + 280000.00 + 3 + 950 + 1 + True + True + 2500 + south + new + + + Historic Listed Building + Charming historic property with character + 300007 + 2027-04-20 + 350000.00 + 4 + 1000 + 3 + False + True + 1800 + west + new + + + Twin Home Units + Duplex with rental income potential + 400008 + 2027-05-10 + 380000.00 + 5 + 1400 + 2 + True + True + 1600 + north + new + + + Contemporary Townhouse + Modern townhouse in trendy neighborhood + 500009 + 2027-06-05 + 320000.00 + 3 + 850 + 3 + True + False + 0 + east + new + + + Lakeside Retreat + Peaceful waterfront property + 600010 + 2027-07-20 + 450000.00 + 3 + 900 + 2 + False + True + 3000 + south + new + + diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..9a2189b6382 --- /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_users diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..4aec67be3bb --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,119 @@ +from dateutil.relativedelta import relativedelta + +from odoo import api, fields, models +from odoo.exceptions import UserError +from odoo.exceptions import ValidationError +from odoo.tools.float_utils import float_compare, float_is_zero + + +class EstateProperty(models.Model): + _name = "estate.property" + _description = "Real estate system" + _order = "id desc" + _inherit = ['mail.thread', 'mail.activity.mixin'] + + _check_expected_price = models.Constraint( + "CHECK(expected_price > 0)", + 'Expected Price must be strictly positive' + ) + _check_selling_price = models.Constraint( + "CHECK(selling_price >= 0)", + 'Selling Price must be positive' + ) + + @api.constrains('selling_price', 'expected_price') + def _check_selling_price(self): + for record in self: + if float_is_zero(record.selling_price, precision_digits=2): + continue + min_price = record.expected_price * 0.90 + if float_compare(record.selling_price, min_price, precision_digits=2) < 0: + raise ValidationError( + "The selling price can't be lower than 90%% of expected price" + ) + + def _get_default_date_calculation(self): + return fields.Date.today() + relativedelta(months=3) + + name = fields.Char(string="Property Name", required=True) + description = fields.Text() + postcode = fields.Char(string="Postal Code") + date_availability = fields.Date(string="Available From", copy=False, default=_get_default_date_calculation) + expected_price = fields.Float(string="Expected Price", required=True) + selling_price = fields.Float(readonly=True, copy=False) + bedrooms = fields.Integer(default=2) + living_area = fields.Integer(string="Living Area (sqm)", help="Living area in square meters") + facades = fields.Integer() + garage = fields.Boolean() + garden = fields.Boolean(string="Garden", help="Has garden") + garden_area = fields.Integer(string="Garden Area (sqm)", help="Garden area in square meters") + garden_orientation = fields.Selection([ + ('north', 'North'), + ('south', 'South'), + ('east', 'East'), + ('west', 'West') + ]) + active = fields.Boolean(default=True, help="Uncheck to archive this property") + state = fields.Selection([ + ('new', 'New'), + ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('cancelled', 'Cancelled'), + ], required=True, copy=False, default='new', tracking=True) + property_type_id = fields.Many2one('estate.property.type', string="Property Type", ondelete='cascade') + buyer_id = fields.Many2one('res.partner', string="Buyer", copy=False) + seller_id = fields.Many2one('res.users', string="Seller", default=lambda self: self.env.user) + tag_ids = fields.Many2many('estate.property.tag', 'estate_property_tag_rel', 'property_id', 'tag_id', string="Tags") + offer_ids = fields.One2many('estate.property.offer', 'property_id', string="Offers") + total_area = fields.Float(compute='_compute_total_area', store=True) + best_price = fields.Float(compute='_compute_best_price', readonly=True, store=True) + + def action_sold(self): + self.ensure_one() + if self.state == 'cancelled': + raise UserError("Cancelled properties cannot be sold.") + self.state = 'sold' + message = "Successfully Sold the product" + if message: + return { + 'effect': { + 'fadeout': 'slow', + 'message': message, + 'img_url': '/web/static/img/smile.svg', + 'type': 'rainbow_man', + } + } + return True + + def action_cancel(self): + for record in self: + if record.state == 'sold': + raise UserError("Sold properties cannot be cancelled.") + record.state = 'cancelled' + return True + + @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.price') + 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 not self.garden: + self.garden_area = 0 + self.garden_orientation = False + else: + self.garden_area = 10 + self.garden_orientation = 'north' + + @api.ondelete(at_uninstall=False) + def _ondelete_check_state(self): + for records in self: + if records.state not in ('new', 'cancelled'): + raise UserError(f"The property state is in {records.state}, you can't delete this property") diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..7ef6c5016c1 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,83 @@ +from datetime import timedelta + +from odoo import api, fields, models +from odoo.exceptions import UserError + + +class EstatePropertyOffer(models.Model): + _name = "estate.property.offer" + _description = "Real estate system - Property Offer" + _order = "price desc" + + _check_offer_price_positive = models.Constraint( + "CHECK(price > 0)", + 'Offer Price must be strictly positive' + ) + + price = fields.Float(string="Offer Price", required=True, copy=False) + status = fields.Selection([ + ('pending', 'Pending'), + ('accepted', 'Accepted'), + ('refused', 'Refused') + ], copy=False, default='pending') + partner_id = fields.Many2one('res.partner', required=True) + property_id = fields.Many2one('estate.property', required=True, ondelete='cascade') + validity = fields.Integer(default=7) + date_deadline = fields.Date( + compute='_compute_date_deadline', + inverse='_inverse_date_deadline' + ) + is_button_hidden = fields.Boolean(compute='_compute_button_visibility', store=False) + property_type_id = fields.Many2one(related='property_id.property_type_id', store=True) + + @api.depends('create_date', 'validity') + def _compute_date_deadline(self): + for offer in self: + date = offer.create_date or fields.Date.today() + offer.date_deadline = date + timedelta(days=offer.validity) + + def _inverse_date_deadline(self): + for record in self: + start = record.create_date.date() if record.create_date else fields.Date.today() + record.validity = (record.date_deadline - start).days + + def action_accept_offer(self): + self.ensure_one() + if self.property_id.state in ('sold', 'cancelled'): + raise UserError("Cannot accept an offer on a sold or cancelled property.") + if self.property_id.offer_ids.filtered(lambda offer: offer.status == 'accepted'): + raise UserError("An offer is already accepted for this property.") + for offer in self.property_id.offer_ids: + if offer.id != self.id: + offer.status = 'refused' + self.status = 'accepted' + self.property_id.selling_price = self.price + self.property_id.buyer_id = self.partner_id.id + self.property_id.state = 'offer_accepted' + return True + + def action_refuse_offer(self): + self.ensure_one() + if self.property_id.state in ('sold', 'cancelled'): + raise UserError("Cannot refuse an offer on a sold or cancelled property.") + if self.status == 'accepted': + raise UserError("Cannot refuse an accepted offer.") + self.status = 'refused' + return True + + @api.model_create_multi + def create(self, vals_list): + for val in vals_list: + prop = self.env['estate.property'].browse(val.get('property_id')) + if val.get('price') < prop.best_price: + raise UserError("The new offer price is less than the existing offered prices") + prop.write({'state': 'offer_received'}) + return super().create(vals_list) + + @api.depends('status', 'property_id.state') + def _compute_button_visibility(self): + for offer in self: + offer.is_button_hidden = ( + offer.status in ('accepted', 'refused') or + offer.property_id.state in ('sold', 'cancelled') + ) diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..3ef805eff61 --- /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 system - Property Tag" + _order = "name" + + _unique_tag_name = models.Constraint( + "UNIQUE(name)", + 'The Property Tag name has to be Unique.' + ) + + name = fields.Char(string="Tag Name", required=True) + color = fields.Integer(string="Color") diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..c7a84116250 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,23 @@ +from odoo import api, fields, models + + +class EstatePropertyType(models.Model): + _name = "estate.property.type" + _description = "Real estate system - Property Type" + _order = "sequence, name" + + _check_type_name = models.Constraint( + "UNIQUE(name)", + 'The Property Type name has to be Unique.' + ) + + name = fields.Char(string="Property Type Name", required=True) + property_ids = fields.One2many('estate.property', 'property_type_id', string="Properties") + sequence = fields.Integer(default=10) + offer_ids = fields.One2many('estate.property.offer', 'property_type_id', string="Offers") + offer_count = fields.Integer(string="Number of Offers", 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_config_settings.py b/estate/models/res_config_settings.py new file mode 100644 index 00000000000..2ba01e80f50 --- /dev/null +++ b/estate/models/res_config_settings.py @@ -0,0 +1,9 @@ +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + module_estate_account = fields.Boolean(string="Enable Invoicing") + + module_estate_auction = fields.Boolean(string="Automated Auction") diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..f826a0c633b --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,12 @@ +from odoo import fields, models + + +class ResUsers(models.Model): + _inherit = 'res.users' + + seller_property_ids = fields.One2many( + 'estate.property', + 'seller_id', + string="Real Estate Properties", + domain=[('state', 'not in', ('sold', 'cancelled'))] + ) diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..6905974b938 --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,9 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_estate_property_manager,estate.property manager,model_estate_property,estate.estate_group_manager,1,1,1,1 +access_estate_property_user,estate.property user,model_estate_property,estate.estate_group_user,1,1,0,0 +access_estate_property_type_manager,estate.property.type manager,model_estate_property_type,estate.estate_group_manager,1,1,1,1 +access_estate_property_type_user,estate.property.type user,model_estate_property_type,estate.estate_group_user,1,0,0,0 +access_estate_property_tag_manager,estate.property.tag manager,model_estate_property_tag,estate.estate_group_manager,1,1,1,1 +access_estate_property_tag_user,estate.property.tag user,model_estate_property_tag,estate.estate_group_user,1,0,0,0 +access_estate_property_offer_manager,estate.property.offer manager,model_estate_property_offer,estate.estate_group_manager,1,1,1,1 +access_estate_property_offer_user,estate.property.offer user,model_estate_property_offer,estate.estate_group_user,1,1,1,0 diff --git a/estate/security/security.xml b/estate/security/security.xml new file mode 100644 index 00000000000..61bad72d4f7 --- /dev/null +++ b/estate/security/security.xml @@ -0,0 +1,29 @@ + + + + + Real Estate + Real Estate Management + + + + Real Estate + + + + + Agent + + + + + Manager + + + + + + + + + diff --git a/estate/static/description/icon.png b/estate/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..6fc6561a21c5f3658dc0cf3e2932c7173fccc6b4 GIT binary patch literal 4777 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 DQny0k literal 0 HcmV?d00001 diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 00000000000..53f245deb28 --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/estate/views/estate_property_offers_views.xml b/estate/views/estate_property_offers_views.xml new file mode 100644 index 00000000000..4846f83c70f --- /dev/null +++ b/estate/views/estate_property_offers_views.xml @@ -0,0 +1,60 @@ + + + + Offers + estate.property.offer + list,form + [('property_type_id', '=', active_id)] + {'default_property_type_id': active_id} + + + + estate.property.offer.form + estate.property.offer + +
+ + + + + + + + + +
+
+
+ + + estate.property.offer.list + estate.property.offer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + estate.property.type.list + estate.property.type + + + + + + + + + + + estate.property.type.search + estate.property.type + + + + + + + + + Property Types + estate.property.type + list,form + +
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..8b1ae9bc885 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,173 @@ + + + + estate.property.form + estate.property + +
+
+
+ +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + + estate.property.list + estate.property + + + + + + + + + + + + + + + + + + + estate.property.kanban + estate.property + + + + + +
+ +
Expected Price:
+
+ Best Price: +
+
+ Selling Price: +
+
+ +
+
+
+
+
+
+
+ + + + estate.property.search + estate.property + + + + + + + + + + + + + + + + + + + + + + + + + + Properties + estate.property + list,kanban,form + estate + {'search_default_properties_with_offers': 1} + +
diff --git a/estate/views/estate_res_users_view.xml b/estate/views/estate_res_users_view.xml new file mode 100644 index 00000000000..362702fc873 --- /dev/null +++ b/estate/views/estate_res_users_view.xml @@ -0,0 +1,29 @@ + + + + res.users.form.inherit.estate + res.users + + + + + + + + + + + + + + + + + + + + + diff --git a/estate/views/res_config_settings.xml b/estate/views/res_config_settings.xml new file mode 100644 index 00000000000..82552e505a5 --- /dev/null +++ b/estate/views/res_config_settings.xml @@ -0,0 +1,24 @@ + + + + res.config.settings.view.form.estate + res.config.settings + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/estate/views/website_menu.xml b/estate/views/website_menu.xml new file mode 100644 index 00000000000..73c5384242e --- /dev/null +++ b/estate/views/website_menu.xml @@ -0,0 +1,9 @@ + + + + Properties + /properties + + 90 + + diff --git a/estate/views/website_property_detail_template.xml b/estate/views/website_property_detail_template.xml new file mode 100644 index 00000000000..26191d866db --- /dev/null +++ b/estate/views/website_property_detail_template.xml @@ -0,0 +1,75 @@ + + + + diff --git a/estate/views/website_property_template.xml b/estate/views/website_property_template.xml new file mode 100644 index 00000000000..788e904a37c --- /dev/null +++ b/estate/views/website_property_template.xml @@ -0,0 +1,39 @@ + + + + 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..2c4b36be1d3 --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,8 @@ +{ + 'name': 'RealEstateAccount', + 'version': '1.0', + 'author': 'Pranjali Sangavekar(prsan)', + 'license': 'LGPL-3', + 'depends': ['estate', 'account'], + 'application': True, +} 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..65121dfa113 --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,25 @@ +from odoo import Command, models + + +class InheritedEstateProperty(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'{record.name} - Commission (6%)', + 'quantity': 1, + 'price_unit': record.selling_price * 0.06, + }), + Command.create({ + 'name': f'{record.name} - Administrative Fees', + 'quantity': 1, + 'price_unit': 100.00, + }), + ], + }) + return super().action_sold() diff --git a/estate_auction/__init__.py b/estate_auction/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/estate_auction/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/estate_auction/__manifest__.py b/estate_auction/__manifest__.py new file mode 100644 index 00000000000..8fb4b0ac501 --- /dev/null +++ b/estate_auction/__manifest__.py @@ -0,0 +1,20 @@ +{ + 'name': 'RealEstateAuction', + 'version': '1.0', + 'depends': ['estate', 'web'], + 'author': 'Pranjali Sangavekar(prsan)', + 'license': 'LGPL-3', + 'application': True, + + 'data': [ + 'views/estate_auction_view.xml', + ], + + 'assets': { + 'web.assets_backend': [ + 'estate_auction/static/src/components/auction_state_selection/auction_state_selection.js', + 'estate_auction/static/src/components/auction_state_selection/auction_state_selection.xml', + 'estate_auction/static/src/components/auction_state_selection/auction_state_selection.scss', + ], + }, +} diff --git a/estate_auction/models/__init__.py b/estate_auction/models/__init__.py new file mode 100644 index 00000000000..f1e9fb549dc --- /dev/null +++ b/estate_auction/models/__init__.py @@ -0,0 +1,2 @@ +from . import estate_auction +from . import estate_auction_offers diff --git a/estate_auction/models/estate_auction.py b/estate_auction/models/estate_auction.py new file mode 100644 index 00000000000..707d8861910 --- /dev/null +++ b/estate_auction/models/estate_auction.py @@ -0,0 +1,22 @@ +from odoo import fields, models + + +class EstateProperty(models.Model): + _inherit = 'estate.property' + + sale_mode = fields.Selection([ + ('regular', 'Regular'), + ('auction', 'Auction'), + ], default='regular', required=True) + auction_end_time = fields.Datetime(string="End Time") + highest_offer = fields.Float(string="Highest Offer", readonly=True) + highest_bidder_id = fields.Many2one('res.partner', string="Highest Bidder", readonly=True) + auction_state = fields.Selection([ + ('template', 'Template'), + ('auction', 'Auction'), + ('sold', 'Sold'), + ], default='template', string="Auction State", store=True, tracking=True) + + def action_start_auction(self): + self.ensure_one() + self.auction_state = 'auction' diff --git a/estate_auction/models/estate_auction_offers.py b/estate_auction/models/estate_auction_offers.py new file mode 100644 index 00000000000..6cb2643ffaa --- /dev/null +++ b/estate_auction/models/estate_auction_offers.py @@ -0,0 +1,32 @@ +from odoo import api, fields, models +from odoo.exceptions import UserError + + +class EstateAuctionOffer(models.Model): + _inherit = 'estate.property.offer' + + is_button_hidden = fields.Boolean(compute='_compute_button_visibility', store=False) + + @api.depends('property_id.sale_mode') + def _compute_button_visibility(self): + for offer in self: + offer.is_button_hidden = ( + offer.property_id.sale_mode == 'auction' + ) + + @api.model_create_multi + def create(self, vals_list): + for val in vals_list: + prop = self.env['estate.property'].browse(val.get('property_id')) + price = val.get('price', 0) + + if prop.sale_mode == 'auction': + if price < prop.expected_price: + raise UserError( + "Auction offer cannot be lower than the expected price." + ) + else: + return super().create(vals_list) + + prop.write({'state': 'offer_received'}) + return models.Model.create(self, vals_list) diff --git a/estate_auction/static/src/components/auction_state_selection/auction_state_selection.js b/estate_auction/static/src/components/auction_state_selection/auction_state_selection.js new file mode 100644 index 00000000000..d66e2c944c5 --- /dev/null +++ b/estate_auction/static/src/components/auction_state_selection/auction_state_selection.js @@ -0,0 +1,55 @@ +import { + StateSelectionField, + stateSelectionField, +} from "@web/views/fields/state_selection/state_selection_field"; + +import { registry } from "@web/core/registry"; + + +export class AuctionStateSelection extends StateSelectionField { + static template = "estate_auction.AuctionStateSelection"; + + setup() { + super.setup(); + + this.icons = { + "template": "fa fa-circle-o", + "auction": "fa fa-check-circle", + "sold": "fa fa-times-circle", + }; + + this.colorIcons = { + "template": "text-secondary", + "auction": "text-success", + "sold": "text-danger", + }; + + this.colorButton = { + "template": "btn-outline-secondary", + "auction": "btn-outline-success", + "sold": "btn-outline-danger", + }; + } + + stateIcon(value) { + return this.icons[value] || ""; + } + + statusColor(value) { + return this.colorIcons[value] || ""; + } + + getTogglerClass(currentValue) { + return ( + "o_state_button btn rounded-pill " + + this.colorButton[currentValue] + ); + } +} + +export const auctionStateSelection = { + ...stateSelectionField, + component: AuctionStateSelection, +}; + +registry.category("fields").add("auction_state_selection", auctionStateSelection); diff --git a/estate_auction/static/src/components/auction_state_selection/auction_state_selection.scss b/estate_auction/static/src/components/auction_state_selection/auction_state_selection.scss new file mode 100644 index 00000000000..e30da9c150c --- /dev/null +++ b/estate_auction/static/src/components/auction_state_selection/auction_state_selection.scss @@ -0,0 +1,20 @@ +.o_state_button { + min-width: 140px; + justify-content: center; +} + +.o_state_button .o_status_label { + white-space: nowrap; +} + +.text-secondary { + color: #6c757d !important; +} + +.text-success { + color: #28a745 !important; +} + +.text-danger { + color: #dc3545 !important; +} \ No newline at end of file diff --git a/estate_auction/static/src/components/auction_state_selection/auction_state_selection.xml b/estate_auction/static/src/components/auction_state_selection/auction_state_selection.xml new file mode 100644 index 00000000000..72c09501d5f --- /dev/null +++ b/estate_auction/static/src/components/auction_state_selection/auction_state_selection.xml @@ -0,0 +1,42 @@ + + + + + + + + + + getTogglerClass(currentValue) + + + + + +
+ + + Auction Stages + +
+
+ + + + + {{ stateIcon(option[0]) }} + + + + + + + {{ statusColor(option[0]) }} + + + +
+ +
\ No newline at end of file diff --git a/estate_auction/views/estate_auction_view.xml b/estate_auction/views/estate_auction_view.xml new file mode 100644 index 00000000000..5024c722c6f --- /dev/null +++ b/estate_auction/views/estate_auction_view.xml @@ -0,0 +1,49 @@ + + + + estate.property.form.auction + estate.property + + + + + + Clear + + + +
From 5840427626bbb22e6da205c2de4a608fd56effb5 Mon Sep 17 00:00:00 2001 From: "Pranjali Sangavekar(prsan)" Date: Fri, 3 Jul 2026 18:01:54 +0530 Subject: [PATCH 4/4] [IMP] estate_auction: add invoice stat button and auction kanban indicator The auction module lacked visibility into related invoices and had no way to identify active auctions directly from the kanban view, forcing users to open each record individually to get this information. - Add invoice smart button on property form to display invoice count and provide quick access to related invoices. - Add auction status indicator on kanban view to visually highlight properties with active auctions. --- estate_auction/models/__init__.py | 1 + estate_auction/models/account_move.py | 7 ++++ estate_auction/models/estate_auction.py | 37 ++++++++++++++++++-- estate_auction/views/estate_auction_view.xml | 37 ++++++++++++++++++++ 4 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 estate_auction/models/account_move.py diff --git a/estate_auction/models/__init__.py b/estate_auction/models/__init__.py index f1e9fb549dc..9428a031a08 100644 --- a/estate_auction/models/__init__.py +++ b/estate_auction/models/__init__.py @@ -1,2 +1,3 @@ from . import estate_auction from . import estate_auction_offers +from . import account_move diff --git a/estate_auction/models/account_move.py b/estate_auction/models/account_move.py new file mode 100644 index 00000000000..0b969023e4f --- /dev/null +++ b/estate_auction/models/account_move.py @@ -0,0 +1,7 @@ +from odoo import fields, models + + +class AccountMove(models.Model): + _inherit = 'account.move' + + estate_property_id = fields.Many2one('estate.property', string="Property") diff --git a/estate_auction/models/estate_auction.py b/estate_auction/models/estate_auction.py index 0870a58467a..8ff4f98d076 100644 --- a/estate_auction/models/estate_auction.py +++ b/estate_auction/models/estate_auction.py @@ -16,6 +16,8 @@ class EstateProperty(models.Model): ('auction', 'Auction'), ('sold', 'Sold'), ], default='template', string="Auction State", store=True, tracking=True) + invoice_ids = fields.One2many('account.move', 'estate_property_id', string="Invoices") + invoice_count = fields.Integer(compute='_compute_invoice_count') def action_start_auction(self): self.ensure_one() @@ -44,9 +46,10 @@ def _cron_close_expired_auctions(self): 'highest_offer': winning_offer.price, 'highest_bidder_id': winning_offer.partner_id.id, 'auction_state': 'sold', - 'state': 'sold' }) + property.action_sold() + def action_send_auction_result_email(self, winning_offer): self.ensure_one() @@ -57,7 +60,7 @@ def action_send_rejected_offer_email(self, winning_offer): self.ensure_one() rejected_template = self.env.ref( - "estate_auction.mail_template_auction_rejected" + 'estate_auction.mail_template_auction_rejected' ) rejected_offers = self.offer_ids.filtered( lambda offer: @@ -70,3 +73,33 @@ def action_send_rejected_offer_email(self, winning_offer): offer.id, force_send=True, ) + + @api.depends('invoice_ids') + def _compute_invoice_count(self): + for record in self: + record.invoice_count = len(record.invoice_ids) + + def action_view_invoices(self): + self.ensure_one() + + return { + 'type': 'ir.actions.act_window', + 'name': "Invoices", + 'res_model': 'account.move', + 'view_mode': 'list,form', + 'domain': [('estate_property_id', '=', self.id)] + } + + def action_sold(self): + res = super().action_sold() + invoice = self.env['account.move'].search( + [ + ('partner_id', '=', self.buyer_id.id), + ('move_type', '=', 'out_invoice') + ], + order='id desc', + limit=1, + ) + if invoice: + invoice.estate_property_id = self.id + return res diff --git a/estate_auction/views/estate_auction_view.xml b/estate_auction/views/estate_auction_view.xml index 8c2c3d28be9..211a3f1756f 100644 --- a/estate_auction/views/estate_auction_view.xml +++ b/estate_auction/views/estate_auction_view.xml @@ -6,6 +6,23 @@ + +
+ +
+
+