Skip to content

[IMP] automated_auction: Improved estate by adding auction feature #503

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 10 commits into
base: 18.0
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions automated_auction/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import controllers
from . import models
32 changes: 32 additions & 0 deletions automated_auction/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
'name': "Automated Auction",
'version': '1.0',
'depends': ['estate', 'estate_account'],
'author': "ppch",
'category': '',
'description': """
Automated Auction module for Estate properties
""",
'license': "LGPL-3",
'data': [
'data/service_cron.xml',
'data/mail_template_data.xml',
'views/estate_property_views.xml',
'views/estate_property_offer_views.xml',
'views/property_add_offer_template.xml',
'views/property_templates.xml',
],
'demo': [
],
'assets': {
'web.assets_frontend': [
'automated_auction/static/src/timer.js',
],
'web.assets_backend': [
'automated_auction/static/src/auction_state_widget/**/*.js',
'automated_auction/static/src/auction_state_widget/**/*.xml',
'automated_auction/static/src/auction_state_widget/**/*.scss',
],
},
'installable': True,
}
2 changes: 2 additions & 0 deletions automated_auction/controllers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import property_list
from . import property_offer
14 changes: 14 additions & 0 deletions automated_auction/controllers/property_list.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from odoo.http import request, route
from odoo.addons.estate.controllers.property_list import EstatePropertyController


class EstatePropertyOfferController(EstatePropertyController):
@route(['/properties', '/properties/page/<int:page>'], type='http', auth="public", website=True)
def list_properties(self, page=1, domain=None, **kwargs):
domain = domain or []

selected_auction_type = kwargs.get('selected_property_auction_type', 'all')
if selected_auction_type != 'all':
domain.append(('property_auction_type', '=', selected_auction_type))

return super().list_properties(page=page, domain=domain, selected_auction_type=selected_auction_type, **kwargs)
31 changes: 31 additions & 0 deletions automated_auction/controllers/property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from odoo import _
from odoo.http import Controller, request, route
from odoo.exceptions import UserError


class EstatePropertyOfferContoller(Controller):
# Route For Add Offer in Particular Property
@route(['/properties/<int:property_id>/add_offer'], type='http', auth="user", website=True)
def add_offer_form(self, property_id, **kwargs):
property_details = request.env['estate.property'].sudo().browse(property_id)

return request.render('automated_auction.property_add_offer_template', {
'property_details': property_details,
})

@route(['/properties/<int:property_id>/submit_offer'], type='http', auth="user", website=True)
def add_offer_submit_form(self, property_id, **post):
property_details = request.env['estate.property'].sudo().browse(property_id)

offer_amount = float(post.get('offer_amount', 0))
if offer_amount <= property_details.expected_price:
raise UserError(_("Price must be greater than or equal to expected price"))

# Create the offer in the estate.property.offer model
request.env['estate.property.offer'].sudo().create({
'property_id': property_id,
'partner_id': request.env.user.partner_id.id,
'price': offer_amount
})

return request.render('automated_auction.property_offer_added')
47 changes: 47 additions & 0 deletions automated_auction/data/mail_template_data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?xml version="1.0" ?>
<odoo>
<data noupdate="1">
<!-- Offer Accepted Email Template -->
<record id="email_template_for_offer_accepted" model="mail.template">
<field name="name">Offer Accepted Notification</field>
<field name="model_id" ref="automated_auction.model_estate_property"/>
<field name="subject">Congratulations! Your Offer for {{ object.name }} is Accepted</field>
<field name="email_to">{{ object.highest_offer_bidder.email }}</field>
<field name="auto_delete" eval="True"/>
<field name="body_html" type="html">
<div>
<p>Dear <t t-out="object.highest_offer_bidder.name"/>,</p>
<p>Congratulations! Your offer for the property <strong><t t-out="object.name"/></strong> has been accepted.</p>
<p>Here are the details of your purchase:</p>
<ul>
<li><strong>Property:</strong> <t t-out="object.name"/></li>
<li><strong>Final Selling Price:</strong> <t t-out="object.selling_price"/> <t t-out="object.company_id.currency_id.symbol"/></li>
</ul>
<p>Our team will contact you soon with further steps.</p>
<p>Thank you for your participation!</p>
<p>Best regards,</p>
<p><strong><t t-out="object.company_id.name"/></strong></p>
</div>
</field>
</record>

<!-- Offer Refused Email Template -->
<record id="email_template_for_offer_refused" model="mail.template">
<field name="name">Offer Refused Notification</field>
<field name="model_id" ref="automated_auction.model_estate_property_offer"/>
<field name="subject">Your Offer for {{ object.property_id.name }} Was Not Accepted</field>
<field name="email_to">{{ object.partner_id.email }}</field>
<field name="auto_delete" eval="True"/>
<field name="body_html" type="html">
<div>
<p>Dear <t t-out="object.partner_id.name"/>,</p>
<p>We regret to inform you that your offer for the property <strong><t t-out="object.property_id.name"/></strong> was not accepted.</p>
<p>We appreciate your participation and hope you find another great property with us.</p>
<p>If you have any questions, feel free to reach out.</p>
<p>Best regards,</p>
<p><strong><t t-out="object.property_id.company_id.name"/></strong></p>
</div>
</field>
</record>
</data>
</odoo>
11 changes: 11 additions & 0 deletions automated_auction/data/service_cron.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="ir_cron_auto_accept_offer" model="ir.cron">
<field name="name">Automated Auction: Accept Highest Offer If auction time is ended</field>
<field name="interval_number">5</field>
<field name="interval_type">minutes</field>
<field name="model_id" ref="automated_auction.model_estate_property"/>
<field name="code">model._auto_accept_property_offer()</field>
<field name="state">code</field>
</record>
</odoo>
2 changes: 2 additions & 0 deletions automated_auction/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import property
from . import property_offer
109 changes: 109 additions & 0 deletions automated_auction/models/property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
from odoo import _, api, fields, models
from odoo.exceptions import UserError


class Property(models.Model):
_inherit = 'estate.property'

property_auction_type = fields.Selection(
string="Auction Type",
help="Automated auction\nRegular auction",
selection=[
('auction', "Auction"),
('regular', "Regular"),
],
required=True,
default='regular',
)
end_time = fields.Datetime()
highest_offer_bidder = fields.Many2one('res.partner', compute='_compute_highest_bidder', readonly=True)
auction_state = fields.Selection([
('in_template', "Template"),
('in_auction', "In Auction"),
('done', "Done"),
], string='State', copy=False, default='in_template', required=True, tracking=True)

@api.depends('offer_ids.price')
def _compute_highest_bidder(self):
'''compute method to compute highest offer bidder'''
for record in self:
highest_offer = max(record.offer_ids, key=lambda o: o.price, default=None)
record.highest_offer_bidder = highest_offer.partner_id if highest_offer else False

def write(self, vals):
'''Write method to prevent auction state update manually'''
if self.env.context.get('bypass_write_check'):
return super().write(vals)

new_auction_state = vals.get('auction_state')
if new_auction_state:
if self.state in ['offer_accepted', 'sold']:
raise UserError(_("You cannot change the state as the auction has ended"))

if new_auction_state == 'in_auction':
self.with_context(bypass_write_check=True).action_start_auction()
vals['auction_state'] = new_auction_state
elif new_auction_state == 'done':
if self.state == 'new':
raise UserError(_("Offer not received yet, you cannot change the state to 'Done'"))
for offer in self.offer_ids:
if offer.price == self.best_price and offer.partner_id == self.highest_offer_bidder:
offer.action_accepted()
vals['auction_state'] = 'done'
break
self.action_send_mail(self.id)
elif new_auction_state == 'in_template':
vals['auction_state'] = 'in_template'
return super().write(vals)

def action_start_auction(self):
'''Action Method for start auction'''
self.ensure_one()
if not self.end_time:
raise UserError(_("Please select Auction End Time first"))
elif self.state in ['sold', 'offer_accepted']:
raise UserError(_("You can not start auction for offer accepted or sold properties"))
elif self.auction_state == 'in_auction':
raise UserError(_("Auction is already going on"))
elif self.auction_state == 'done':
raise UserError(_("Auction ended already"))
self.with_context(bypass_write_check=True).write({'auction_state': 'in_auction'})

def _auto_accept_property_offer(self):
'''cron method to check auction ended or not and ended then set values'''
auction_ended_properties = self.search([
('end_time', '<', fields.Datetime.now()),
('state', '=', 'offer_received')
])
for property in auction_ended_properties:
for offer in property.offer_ids:
if offer.price == property.best_price and offer.partner_id == property.highest_offer_bidder:
offer.action_accepted()
property.with_context(bypass_write_check=True).write({'auction_state': 'done'})
break
self.action_send_mail(property.id)

auction_ended_but_no_offers = self.search([
('end_time', '<', fields.Datetime.now()),
('state', '=', 'new')
])
for property in auction_ended_but_no_offers:
property.auction_state = 'in_template'

def action_send_mail(self, property_id):
'''method to send mail to the all participants of auction'''
property = self.env['estate.property'].browse(property_id)

offer_accepted_participant = property.highest_offer_bidder
offer_refused = self.env['estate.property.offer'].search([
('property_id', '=', property_id),
('status', '=', 'refused')
])

template_offer_accepted = self.env.ref('automated_auction.email_template_for_offer_accepted')
template_offer_refused = self.env.ref('automated_auction.email_template_for_offer_refused')

if offer_accepted_participant:
template_offer_accepted.send_mail(property_id, force_send=True)
for offer in offer_refused:
template_offer_refused.send_mail(offer.id, force_send=True)
47 changes: 47 additions & 0 deletions automated_auction/models/property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from odoo import _, api, fields, models
from odoo.exceptions import UserError


class PropertyOffer(models.Model):
_inherit = 'estate.property.offer'

is_auction = fields.Selection(related='property_id.property_auction_type')

# CRUD
@api.model_create_multi
def create(self, vals_list):
'''overriding create method to create offer which is less than best price
which will only work if property_auction_type is auction'''
property_price_list = []
for vals in vals_list:
property = self.env['estate.property'].browse(vals.get('property_id'))
actual_price = vals.get('price', 0)
property_price_list.append(actual_price)
vals['price'] = actual_price if actual_price > property.best_price else property.best_price + 1

offers = super().create(vals_list)

for offer, actual_price in zip(offers, property_price_list):
if actual_price is not None:
offer._generate_price(property_price_vals=actual_price)

property = offer.property_id

if property.property_auction_type == 'regular' and property.best_price > actual_price:
raise UserError(_(f"A higher offer already exists, increase your offer price.\n(It should be more than {property.best_price})"))
elif property.property_auction_type == 'auction' and property.auction_state != 'in_auction':
raise UserError(_("Auction isn't started yet."))
elif property.property_auction_type == 'auction' and property.end_time < fields.Datetime.now():
raise UserError(_("Auction time Ended."))
elif property.expected_price > actual_price:
raise UserError(_("You can not add offer less than Expected price"))

if actual_price > property.best_price:
property.highest_offer_bidder = offer.partner_id
property.state = 'offer_received'
return offers

def _generate_price(self, property_price_vals=None):
'''helper method to reset price'''
if property_price_vals:
self.write({'price': property_price_vals})
Loading