chore: import enterprise addons 19.0 bulk snapshot

This commit is contained in:
1b-agent committed 2026-09-16 10:13:08 +00:00
commit ca4af030d3
39951 files changed
+15570005

No files matched your search

+11
View File
@@ -0,0 +1,11 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import product_pricelist
from . import product_pricing
from . import product_product
from . import product_template
from . import res_company
from . import res_config_settings
from . import sale_order
from . import sale_order_line
from . import website
@@ -0,0 +1,11 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class ProductPricelist(models.Model):
_inherit = "product.pricelist"
def _enable_rental_price(self, *args, **kwargs):
""" Override to force the computation through rental price from website """
return super()._enable_rental_price(*args, **kwargs) or self.env.context.get('website_id')
@@ -0,0 +1,12 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class ProductPricing(models.Model):
_inherit = 'product.pricing'
def _get_tz(self):
if website := self.env["website"].get_current_website(fallback=False):
return website.tz
return super()._get_tz()
@@ -0,0 +1,60 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from pytz import timezone
from odoo import models
from odoo.http import request
class ProductProduct(models.Model):
_inherit = 'product.product'
def _website_show_quick_add(self):
return super()._website_show_quick_add() and self._can_be_added_to_current_cart()
def _is_add_to_cart_allowed(self):
return super()._is_add_to_cart_allowed() and self._can_be_added_to_current_cart()
# TODO VFE: clean mess someday, too many methods to check product can be added or not
# TODO provide a way to return error message instead, cause 'product doesn't exist' is bof bof
def _can_be_added_to_current_cart(self):
# Overnight products can only be added to:
# - an empty cart
# - a cart with the same product
# - a cart with the same overnight period
if (
not self.rent_ok
or not (cart := request.cart)
or not (cart.rental_start_date and cart.rental_return_date)
):
# Not a rental product, no cart, or no rentals in the current cart
return True
if cart.order_line.filtered('is_rental').product_id == self:
# Only rental product in cart is the current product so we allow any period change
return True
best_pricing = self._get_best_pricing_rule(
start_date=cart.rental_start_date,
end_date=cart.rental_return_date,
pricelist=request.pricelist,
currency=request.website.currency_id,
)
pricing_recurrence = best_pricing.recurrence_id
if not pricing_recurrence or not pricing_recurrence.overnight:
# Not an overnight pricing: not an issue (worst case the customer is overpriced)
return True
website_tz = timezone(request.website.tz)
pickup_time, return_time = pricing_recurrence.pickup_time, pricing_recurrence.return_time
pickup_date, return_date = (
cart.rental_start_date.astimezone(website_tz),
cart.rental_return_date.astimezone(website_tz),
)
return (
(pickup_date.hour + pickup_date.minute / 60) == pickup_time
and (return_date.hour + return_date.minute / 60) == return_time
and pickup_date.day != return_date.day
)
@@ -0,0 +1,309 @@
# 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()
@@ -0,0 +1,42 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from dateutil.relativedelta import relativedelta
from odoo import fields, models
class ResCompany(models.Model):
_inherit = 'res.company'
renting_minimal_time_duration = fields.Integer("Minimal Rental Duration")
renting_minimal_time_unit = fields.Selection([
("hour", "Hours"),
("day", "Days"),
("week", "Weeks"),
("month", "Months")
], string="Minimal Rental Duration Unit", required=True, default='day')
renting_forbidden_mon = fields.Boolean("Monday")
renting_forbidden_tue = fields.Boolean("Tuesday")
renting_forbidden_wed = fields.Boolean("Wednesday")
renting_forbidden_thu = fields.Boolean("Thursday")
renting_forbidden_fri = fields.Boolean("Friday")
renting_forbidden_sat = fields.Boolean("Saturday")
renting_forbidden_sun = fields.Boolean("Sunday")
def _get_minimal_rental_duration(self):
"""Get minimal rental duration expressed in relativedelta"""
return relativedelta(
**{f'{self.renting_minimal_time_unit}s': self.renting_minimal_time_duration}
)
def _get_renting_forbidden_days(self):
forbidden_day_fields = {
'renting_forbidden_mon': 1,
'renting_forbidden_tue': 2,
'renting_forbidden_wed': 3,
'renting_forbidden_thu': 4,
'renting_forbidden_fri': 5,
'renting_forbidden_sat': 6,
'renting_forbidden_sun': 7,
}
return [value for field, value in forbidden_day_fields.items() if self[field]]
@@ -0,0 +1,27 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
renting_minimal_time_duration = fields.Integer(
related='company_id.renting_minimal_time_duration', readonly=False)
renting_minimal_time_unit = fields.Selection(
related='company_id.renting_minimal_time_unit', readonly=False, required=True)
renting_forbidden_mon = fields.Boolean(
"Monday", related="company_id.renting_forbidden_mon", readonly=False)
renting_forbidden_tue = fields.Boolean(
"Tuesday", related="company_id.renting_forbidden_tue", readonly=False)
renting_forbidden_wed = fields.Boolean(
"Wednesday", related="company_id.renting_forbidden_wed", readonly=False)
renting_forbidden_thu = fields.Boolean(
"Thursday", related="company_id.renting_forbidden_thu", readonly=False)
renting_forbidden_fri = fields.Boolean(
"Friday", related="company_id.renting_forbidden_fri", readonly=False)
renting_forbidden_sat = fields.Boolean(
"Saturday", related="company_id.renting_forbidden_sat", readonly=False)
renting_forbidden_sun = fields.Boolean(
"Sunday", related="company_id.renting_forbidden_sun", readonly=False)
tz = fields.Selection(related='website_id.tz', readonly=False)
+253
View File
@@ -0,0 +1,253 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
import pytz
from odoo import fields, models
from odoo.exceptions import UserError, ValidationError
from odoo.tools.date_utils import end_of, float_to_time, localized, start_of, to_timezone
class SaleOrder(models.Model):
_inherit = 'sale.order'
def _is_cart_ready(self):
"""Whether the cart is valid and can be confirmed (and paid for)
:rtype: bool
"""
res = super()._is_cart_ready()
return res and (not self.is_rental_order or self._available_dates_for_renting())
def _check_cart_is_ready_to_be_paid(self):
self.ensure_one()
if not self._available_dates_for_renting():
raise ValidationError(self.env._(
"Some of your rental products cannot be rented during the selected period and your"
" cart must be updated. We're sorry for the inconvenience."
))
return super()._check_cart_is_ready_to_be_paid()
def _verify_updated_quantity(self, order_line, product_id, new_qty, uom_id, **kwargs):
new_qty, warning = super()._verify_updated_quantity(
order_line, product_id, new_qty, uom_id, **kwargs
)
product = self.env['product.product'].browse(product_id)
# FIXME doesn't make sense to check the cart config here
# If you add a rental product to a (now) invalid cart, the product won't be added/will be
# removed, but existing products will stay, strange behavior
if new_qty > 0 and product.rent_ok and not self._is_valid_renting_dates():
self.shop_warning = self._build_warning_renting(product)
return 0, self.shop_warning
return new_qty, warning
def convert_to_website_tz(self, value):
""" Convert a given naive datetime into the website's timezone.
The naive datetime value is assumed to be in UTC timezone (default database format).
:param datetime value: The date to convert.
:return: naive datetime expressed in the website timezone.
:rtype: naive datetime
"""
return pytz.utc.localize(value).astimezone(
pytz.timezone(self.website_id.tz)
).replace(tzinfo=None)
def _is_valid_renting_dates(self):
""" Check if the pickup and return dates are valid.
:return: Whether the pickup and return dates are valid.
:rtype: bool
"""
self.ensure_one()
if not self.has_rented_products:
return True
if not (self.rental_start_date and self.rental_return_date):
return False
days_forbidden = self.company_id._get_renting_forbidden_days()
converted_rental_start_date = self.convert_to_website_tz(self.rental_start_date)
converted_rental_return_date = self.convert_to_website_tz(self.rental_return_date)
converted_current_datetime = self.convert_to_website_tz(fields.Datetime.now())
return (
# 15 minutes of allowed time between adding the product to cart and paying it.
converted_rental_start_date >= converted_current_datetime - timedelta(minutes=15)
and converted_rental_start_date.isoweekday() not in days_forbidden
and converted_rental_return_date.isoweekday() not in days_forbidden
and self._get_renting_duration() >= self.company_id.renting_minimal_time_duration
)
def _cart_add(self, product_id, *args, start_date=None, end_date=None, **kwargs):
product = self.env['product.product'].browse(product_id)
if product.rent_ok:
first_pricing = self.env["product.pricing"]._get_first_suitable_pricing(
product, pricelist=self.pricelist_id
)
dates_provided = start_date and end_date
if not dates_provided:
# Compute default dates based on the first suitable pricing (rental prices)
if first_pricing:
rec = first_pricing.recurrence_id
start_date, end_date = product.product_tmpl_id._get_default_renting_dates(
start_date, end_date, rec.duration, rec.unit, rec.pickup_time, rec.return_time
)
# Fallback to the next 24 hours (_rental_set_dates default)
else:
start_date = fields.Datetime.now().replace(minute=0, second=0) \
+ timedelta(hours=1)
end_date = start_date + timedelta(days=1)
recurrence = first_pricing.recurrence_id
if dates_provided and recurrence and self.website_id.tz:
website_tz = ZoneInfo(self.website_id.tz)
to_website_tz = to_timezone(website_tz)
to_naive_utc = to_timezone(None)
loc_start = to_website_tz(localized(start_date))
loc_end = to_website_tz(localized(end_date))
if recurrence.overnight:
start_date = to_naive_utc(
datetime.combine(
loc_start, float_to_time(recurrence.pickup_time), tzinfo=website_tz
)
)
end_date = to_naive_utc(
datetime.combine(
loc_end, float_to_time(recurrence.return_time), tzinfo=website_tz
)
)
elif recurrence.unit != "hour":
start_date = to_naive_utc(start_of(loc_start, "day").replace(tzinfo=website_tz))
end_date = to_naive_utc(end_of(loc_end, "day").replace(tzinfo=website_tz))
if (
self.rental_start_date and self.rental_return_date
and (
self.rental_start_date != start_date
or self.rental_return_date != end_date
)
):
raise UserError(self.env._(
"You cannot mix different rental periods in the same order."
))
else:
self.update({
'rental_start_date': start_date.replace(tzinfo=None),
'rental_return_date': end_date.replace(tzinfo=None),
})
return super()._cart_add(
product_id, *args, start_date=start_date, end_date=end_date, **kwargs
)
def _create_new_cart_line(self, *args, **kwargs):
# Make sure that if the product is rentable, the line created is a rental line.
return super(SaleOrder, self.with_context(in_rental_app=True))._create_new_cart_line(
*args, **kwargs
)
def _verify_cart_after_update(self, *args, **kwargs):
super()._verify_cart_after_update(*args, **kwargs)
if self.is_rental_order and not self.has_rented_products:
self.write({
'is_rental_order': False,
'rental_start_date': False,
'rental_return_date': False,
})
def _build_warning_renting(self, product):
""" Build the renting warning on SO to warn user a product cannot be rented on that period.
Note: self.ensure_one()
:param ProductProduct product: The product concerned by the warning
"""
self.ensure_one()
if not (self.rental_start_date and self.rental_return_date):
return self.env._("No dates were specified on your rental order.")
company = self.company_id
days_forbidden = company._get_renting_forbidden_days()
pickup_forbidden = self.rental_start_date.isoweekday() in days_forbidden
return_forbidden = self.rental_return_date.isoweekday() in days_forbidden
message = self.env._("""
Some of your rental products (%(product)s) cannot be rented during the
selected period and your cart must be updated. We're sorry for the
inconvenience.
""", product=product.name)
if self.rental_start_date < fields.Datetime.now():
message += self.env._("""Your rental product cannot be pickedup in the past.""")
elif pickup_forbidden and return_forbidden:
message += self.env._("""
Your rental product had invalid dates of pickup (%(start_date)s) and
return (%(end_date)s). Unfortunately, we do not process pickups nor
returns on those weekdays.
""", start_date=self.rental_start_date, end_date=self.rental_return_date)
elif pickup_forbidden:
message += self.env._("""
Your rental product had invalid date of pickup (%(start_date)s).
Unfortunately, we do not process pickups on that weekday.
""", start_date=self.rental_start_date)
elif return_forbidden:
message += self.env._("""
Your rental product had invalid date of return (%(end_date)s).
Unfortunately, we do not process returns on that weekday.
""", end_date=self.rental_return_date)
minimal_duration = company.renting_minimal_time_duration
if self._get_renting_duration() < minimal_duration:
message += self.env._("""
Your rental duration was too short. Unfortunately, we do not process
rentals that last less than %(duration)s %(unit)s.
""", duration=minimal_duration, unit=company.renting_minimal_time_unit)
return message
def _get_renting_duration(self):
""" Return the renting rounded-up duration. """
return self.env['product.pricing']._compute_duration_vals(
self.rental_start_date, self.rental_return_date
)[self.company_id.renting_minimal_time_unit]
def _is_renting_possible_in_hours(self):
""" Whether all products in the cart can be rented in a period computed in hours. """
rental_order_lines = self.order_line.filtered('is_rental')
return all('hour' in line.product_id.product_pricing_ids.mapped('recurrence_id.unit')
for line in rental_order_lines)
def _all_hourly_periods_are_overnight(self):
"""Whether the cart contains hourly periods all overnight."""
rental_order_lines = self.order_line.filtered('is_rental')
for line in rental_order_lines:
for pricing in line.product_id.product_pricing_ids:
if pricing.recurrence_id.unit != 'hour' or not pricing.recurrence_id.overnight:
return False
return True
def _cart_update_renting_period(self, start_date, end_date):
self.ensure_one()
current_start_date = self.rental_start_date
current_end_date = self.rental_return_date
self.write({
'rental_start_date': start_date,
'rental_return_date': end_date,
})
if not self._available_dates_for_renting():
# shop_warning can be set by stock if invalid dates
self.shop_warning = self.shop_warning or self.env._(
"The new period is not valid for some products of your cart."
" Your changes on the rental period are not taken into account."
)
self.write({
'rental_start_date': current_start_date,
'rental_return_date': current_end_date,
})
else:
rental_lines = self.order_line.filtered('is_rental')
self.env.add_to_compute(rental_lines._fields['name'], rental_lines)
self._recompute_rental_prices()
rental_lines = self.order_line.filtered('is_rental')
rental_lines._compute_name()
def _available_dates_for_renting(self):
"""Hook to override with the stock availability"""
return self._is_valid_renting_dates()
@@ -0,0 +1,39 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'
def _get_rental_pricing_description(self):
self.ensure_one()
order = self.order_id
pricing = self.product_id._get_best_pricing_rule(
start_date=order.rental_start_date,
end_date=order.rental_return_date,
pricelist=order.pricelist_id,
company=order.company_id,
currency=order.currency_id,
)
return pricing.description
def _get_tz(self):
return self.order_id.website_id.tz or super()._get_tz()
def _is_reorder_allowed(self):
if self.is_rental:
return False
return super()._is_reorder_allowed()
def _get_rental_order_line_description(self):
""" Add timezone of website to sale order line description
:return: order line description after adding timezone of website
:rtype: string
"""
order_line_description = super()._get_rental_order_line_description()
website = self.order_id.website_id
if website and not website._is_customer_in_the_same_timezone():
order_line_description += f' ({self.order_id.website_id.tz})'
return order_line_description
+55
View File
@@ -0,0 +1,55 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import pytz
from odoo import fields, models
from odoo.fields import Domain
from odoo.http import request
class Website(models.Model):
_inherit = 'website'
tz = fields.Selection(
selection='_tz_get',
required=True,
default=lambda self: self.env.user.tz or 'UTC',
string="Timezone",
help="Select your website timezone here."
)
def _tz_get(self):
return [
(tz, f'{tz} {self._get_utc_offset(tz)}')
for tz in sorted(
pytz.all_timezones, key=lambda tz: tz if not tz.startswith('Etc/') else '_'
)
]
def _product_domain(self):
return Domain.OR([[('rent_ok', '=', True)], super()._product_domain()])
def _is_customer_in_the_same_timezone(self):
""" Return whether the customer is on the same timezone as the website or not.
Compare the timezone offset between the website and the customer's browser.
:return: Whether the customer is on the same timezone as the website or not.
:rtype: bool
"""
now = fields.Datetime.now()
customer_tz = request.cookies.get('tz') if request else None
return (
pytz.timezone(self.tz).localize(now).utcoffset()
== pytz.timezone(customer_tz or 'UTC').localize(now).utcoffset()
)
def _get_utc_offset(self, tz):
""" Return the offset between UTC and the provided timezone
:return: (UTC ±HH:MM)
:rtype: string
"""
# strftime('%z') return the UTC offset in this form: ±HHMM[SS[.ffffff]]
utcoffset = pytz.timezone(tz).localize(fields.Datetime.now()).strftime('%z')
return f'(UTC {utcoffset[0]} {utcoffset[1:3]}:{utcoffset[3:5]})'