Files
enterprise-addons/website_sale_renting/models/product_template.py
T

310 lines
14 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
from math import ceil, floor
from dateutil.relativedelta import relativedelta
from pytz import UTC, timezone
from odoo import _, api, fields, models
from odoo.exceptions import UserError
from odoo.http import request
from odoo.tools import format_amount
from odoo.addons.sale_renting.models.product_pricing import PERIOD_RATIO
class ProductTemplate(models.Model):
_inherit = 'product.template'
def _get_additionnal_combination_info(self, product_or_template, quantity, uom, date, website):
"""Override to add the information about renting for rental products
If the product is rent_ok, this override adds the following information about the rental:
- is_rental: Whether combination is rental,
- rental_duration: The duration of the first defined product pricing on this product
- rental_unit: The unit of the first defined product pricing on this product
- default_start_date: If no pickup nor rental date in context, the start_date of the
first renting sale order line in the cart;
- default_end_date: If no pickup nor rental date in context, the end_date of the
first renting sale order line in the cart;
- current_rental_duration: If no pickup nor rental date in context, see rental_duration,
otherwise, the duration between pickup and rental date in the
current_rental_unit unit.
- current_rental_unit: If no pickup nor rental date in context, see rental_unit,
otherwise the unit of the best pricing for the renting between
pickup and rental date.
- current_rental_price: If no pickup nor rental date in context, see price,
otherwise the price of the best pricing for the renting between
pickup and rental date.
"""
res = super()._get_additionnal_combination_info(product_or_template, quantity, uom, date, website)
if not product_or_template.rent_ok:
return res
res['list_price'] = res['price'] # No pricelist discount for rental prices
currency = website.currency_id.with_context(self.env.context)
pricelist = request.pricelist.with_context(self.env.context)
ProductPricing = self.env['product.pricing']
pricing = ProductPricing._get_first_suitable_pricing(product_or_template, pricelist)
if not pricing:
return res
# Compute best pricing rule or set default
order_sudo = request and request.cart or self.env['sale.order'].sudo()
start_date = order_sudo.rental_start_date or self.env.context.get('start_date')
end_date = order_sudo.rental_return_date or self.env.context.get('end_date')
if start_date and end_date:
current_pricing = product_or_template._get_best_pricing_rule(
start_date=start_date,
end_date=end_date,
pricelist=pricelist,
currency=currency,
)
current_unit = current_pricing.recurrence_id.unit
current_duration = ProductPricing._compute_duration_vals(
start_date, end_date
)[current_unit]
# Ensure minimum duration is 24 (hours) to ensure the return date is start date + 1
# Needed when the start date / end date are taken from the cart
if current_pricing.recurrence_id.overnight:
current_duration = max(current_duration, 24)
else:
current_unit = pricing.recurrence_id.unit
current_duration = pricing.recurrence_id.duration
current_pricing = pricing
# Compute current price & set default times
pickup_time = current_pricing.recurrence_id.pickup_time
return_time = current_pricing.recurrence_id.return_time
start_date, end_date = self._get_default_renting_dates(
start_date, end_date, current_duration, current_unit, pickup_time, return_time
)
# Here we don't add the current_attributes_price_extra nor the
# no_variant_attributes_price_extra to the context since those prices are not added
# in the context of rental.
current_price = pricelist._get_product_price(
product=product_or_template,
quantity=quantity,
currency=currency,
start_date=start_date,
end_date=end_date,
)
ratio = ceil(current_duration) / pricing.recurrence_id.duration if pricing.recurrence_id.duration else 1
if current_unit != pricing.recurrence_id.unit:
ratio *= PERIOD_RATIO[current_unit] / PERIOD_RATIO[pricing.recurrence_id.unit]
# apply taxes
product_taxes = res['product_taxes']
if product_taxes:
current_price = self.env['product.template']._apply_taxes_to_price(
current_price, currency, product_taxes, res['taxes'], product_or_template,
)
suitable_pricings = ProductPricing._get_suitable_pricings(product_or_template, pricelist)
# If there are multiple pricings with the same recurrence, we only keep the cheapest ones
best_pricings = {}
for p in suitable_pricings:
if p.recurrence_id not in best_pricings:
best_pricings[p.recurrence_id] = p
elif best_pricings[p.recurrence_id].price > p.price:
best_pricings[p.recurrence_id] = p
suitable_pricings = best_pricings.values()
def _pricing_price(pricing):
if product_taxes:
price = self.env['product.template']._apply_taxes_to_price(
pricing.price, currency, product_taxes, res['taxes'], product_or_template
)
else:
price = pricing.price
if pricing.currency_id == currency:
return price
return pricing.currency_id._convert(
from_amount=price,
to_currency=currency,
company=self.env.company,
date=date,
)
pricing_table = [
(p.name, format_amount(self.env, _pricing_price(p), currency))
for p in suitable_pricings
]
recurrence = pricing.recurrence_id
current_duration, current_rental_unit = (
current_pricing.recurrence_id._get_converted_duration_and_label(current_duration)
)
return {
**res,
'is_rental': True,
'rental_duration': recurrence.duration,
'rental_duration_unit': recurrence.unit,
'rental_unit': recurrence._get_unit_label(recurrence.duration),
'overnight_period': recurrence.overnight,
'default_start_date': start_date,
'default_end_date': end_date,
'current_rental_duration': ceil(current_duration),
'current_rental_unit': current_rental_unit,
'current_rental_price': current_price,
'current_rental_price_per_unit': current_price / (ratio or 1),
'base_unit_price': 0,
'base_unit_name': False,
'pricing_table': pricing_table,
'prevent_zero_price_sale': website.prevent_zero_price_sale and currency.is_zero(
current_price,
),
}
@api.model
def _get_default_renting_dates(self, start_date, end_date, duration, unit, pickup_time, return_time):
""" Get default renting dates to help user
:param datetime start_date: a start_date which is directly returned if defined
:param datetime end_date: a end_date which is directly returned if defined
:param int duration: the duration expressed in int, in the unit given
:param string unit: The duration unit, which can be 'hour', 'day', 'week' or 'month'
:param float pickup_time: The pickup time in hours (0-23.99)
:param float return_time: The return time in hours (0-23.99)
"""
if start_date and end_date and start_date >= end_date:
raise UserError(_("Please choose a return date that is after the pickup date."))
if start_date or end_date:
return start_date, end_date
default_start_dt = self._get_default_start_date(pickup_time)
if unit == 'hour':
default_end_dt = self._get_default_end_date(
default_start_dt,
duration,
unit,
return_time,
)
else:
# If unit in day, week, month, take into account the entire day.
# 21st + 1 day --> from 21st 00:00:00 to 22nd 23:59:59
default_start_dt = datetime.combine(default_start_dt.date(), datetime.min.time())
# remove a second to avoid adding a day (from date point of view)
default_end_dt = self._get_default_end_date(
default_start_dt + relativedelta(seconds=-1),
duration,
unit,
return_time,
)
# Consider the timezone if frontend request
# Return the UTC value according to the client
# because the frontend will convert values according to its timezone
# (and without conversion, we risk changing day).
if request and request.is_frontend and request.cookies.get('tz'):
client_tz = timezone(request.cookies['tz'])
default_start_dt = client_tz.localize(default_start_dt).astimezone(UTC)
default_end_dt = client_tz.localize(default_end_dt).astimezone(UTC)
return default_start_dt, default_end_dt
@api.model
def _get_default_start_date(self, pickup_time):
""" Get the default pickup date and make it extensible """
first_date = self._get_first_potential_date(
fields.Datetime.now() + relativedelta(days=1, hours=1, minute=0, second=0, microsecond=0)
)
if pickup_time:
website_tz = timezone(request.website.tz)
# convert first_date in website TZ before replacing the hours
pickup_hour = floor(pickup_time)
first_date_website_tz = website_tz.localize(first_date).replace(
hour=pickup_hour,
minute=round((pickup_time - pickup_hour) * 60),
)
# send it back in UTC (naive)
first_date = first_date_website_tz.astimezone(UTC).replace(tzinfo=None)
return first_date
@api.model
def _get_default_end_date(self, start_date, duration, unit, return_time):
""" Get the default return date based on pickup date and duration
:param datetime start_date: the default start_date
:param int duration: the duration expressed in int, in the unit given
:param string unit: The duration unit, which can be 'hour', 'day', 'week' or 'month'
"""
return_date = self._get_first_potential_date(max(
start_date + relativedelta(**{f'{unit}s': duration}),
start_date + self.env.company._get_minimal_rental_duration(),
))
if return_time:
website_tz = timezone(request.website.tz)
# convert return_date in website TZ before replacing the hours
return_hour = floor(return_time)
return_date_website_tz = return_date.astimezone(website_tz).replace(
hour=return_hour,
minute=round((return_time - return_hour) * 60),
)
# send it back in UTC (naive)
return_date = return_date_website_tz.astimezone(UTC).replace(tzinfo=None)
return return_date
@api.model
def _get_first_potential_date(self, date):
""" Get the first potential date which respects company unavailability days settings
"""
days_forbidden = self.env.company._get_renting_forbidden_days()
weekday = date.isoweekday()
for i in range(7):
if ((weekday + i) % 7 or 7) not in days_forbidden:
break
return date + relativedelta(days=i)
def _search_render_results_prices(self, mapping, combination_info):
if not combination_info.get('is_rental'):
return super()._search_render_results_prices(mapping, combination_info)
duration = combination_info['rental_duration']
unit = combination_info['rental_unit']
if combination_info['overnight_period']:
duration = combination_info['current_rental_duration']
unit = combination_info['current_rental_unit']
return self.env['ir.ui.view']._render_template(
'website_sale_renting.rental_search_result_price',
values={
'currency': mapping['detail']['display_currency'],
'price': combination_info['price'],
'duration': duration,
'unit': unit,
}
), None
def _get_sales_prices(self, website):
prices = super()._get_sales_prices(website)
pricelist = request.pricelist
for template in self:
if not template.rent_ok:
continue
pricing = self.env['product.pricing']._get_first_suitable_pricing(template, pricelist)
if pricing:
recurrence = pricing.recurrence_id
duration, unit = recurrence._get_converted_duration_and_label(recurrence.duration)
prices[template.id]['rental_duration'] = duration
prices[template.id]['rental_unit'] = unit
else:
prices[template.id]['rental_duration'] = 0
prices[template.id]['rental_unit'] = False
return prices
def _search_get_detail(self, website, order, options):
search_details = super()._search_get_detail(website, order, options)
if options.get('rent_only') or (options.get('from_date') and options.get('to_date')):
search_details['base_domain'].append([('rent_ok', '=', True)])
return search_details
def _website_show_quick_add(self):
return super()._website_show_quick_add() and self.product_variant_id._can_be_added_to_current_cart()