1
0
forked from Mapan/odoo17e
odoo17e-kedaikipas58/addons/sale_renting/models/sale_order.py
2024-12-10 09:04:09 +07:00

291 lines
11 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from dateutil.relativedelta import relativedelta
from math import ceil
from odoo import _, api, fields, models
from odoo.osv import expression
from odoo.tools import float_compare
RENTAL_STATUS = [
('draft', "Quotation"),
('sent', "Quotation Sent"),
('pickup', "Reserved"),
('return', "Pickedup"),
('returned', "Returned"),
('cancel', "Cancelled"),
]
class SaleOrder(models.Model):
_inherit = 'sale.order'
_sql_constraints = [(
'rental_period_coherence',
"CHECK(rental_start_date < rental_return_date)",
"The rental start date must be before the rental return date if any.",
)]
#=== FIELDS ===#
is_rental_order = fields.Boolean(
string="Created In App Rental",
compute='_compute_is_rental_order',
store=True, precompute=True, readonly=False,
# By default, all orders created in rental app are Rental Orders
default=lambda self: self.env.context.get('in_rental_app'))
has_rented_products = fields.Boolean(compute='_compute_has_rented_products')
rental_start_date = fields.Datetime(string="Rental Start Date", tracking=True)
rental_return_date = fields.Datetime(string="Rental Return Date", tracking=True)
duration_days = fields.Integer(
string="Duration in days",
compute='_compute_duration',
help="The duration in days of the rental period.",
)
remaining_hours = fields.Integer(
string="Remaining duration in hours",
compute='_compute_duration',
help="The leftover hours of the rental period.",
)
show_update_duration = fields.Boolean(string="Has Duration Changed", store=False)
rental_status = fields.Selection(
selection=RENTAL_STATUS,
string="Rental Status",
compute='_compute_rental_status',
store=True)
# rental_status = next action to do basically, but shown string is action done.
next_action_date = fields.Datetime(
string="Next Action", compute='_compute_rental_status', store=True)
has_pickable_lines = fields.Boolean(compute='_compute_has_action_lines')
has_returnable_lines = fields.Boolean(compute='_compute_has_action_lines')
is_late = fields.Boolean(
string="Is overdue",
help="The products haven't been picked-up or returned in time",
compute='_compute_is_late',
)
#=== COMPUTE METHODS ===#
@api.depends('order_line.is_rental')
def _compute_is_rental_order(self):
for order in self:
# If a rental product is added in the rental app to the order, it becomes a rental order
order.is_rental_order = order.is_rental_order or order.has_rented_products
@api.depends('order_line.is_rental')
def _compute_has_rented_products(self):
for so in self:
so.has_rented_products = any(line.is_rental for line in so.order_line)
@api.depends('rental_start_date', 'rental_return_date')
def _compute_duration(self):
self.duration_days = 0
self.remaining_hours = 0
for order in self:
if order.rental_start_date and order.rental_return_date:
duration = order.rental_return_date - order.rental_start_date
order.duration_days = duration.days
order.remaining_hours = ceil(duration.seconds / 3600)
@api.depends(
'rental_start_date',
'rental_return_date',
'state',
'order_line.is_rental',
'order_line.product_uom_qty',
'order_line.qty_delivered',
'order_line.qty_returned',
)
def _compute_rental_status(self):
self.next_action_date = False
for order in self:
if not order.is_rental_order:
order.rental_status = False
elif order.state != 'sale':
order.rental_status = order.state
elif order.has_pickable_lines:
order.rental_status = 'pickup'
order.next_action_date = order.rental_start_date
elif order.has_returnable_lines:
order.rental_status = 'return'
order.next_action_date = order.rental_return_date
else:
order.rental_status = 'returned'
@api.depends(
'is_rental_order',
'state',
'order_line.is_rental',
'order_line.product_uom_qty',
'order_line.qty_delivered',
'order_line.qty_returned',
)
def _compute_has_action_lines(self):
self.has_pickable_lines = False
self.has_returnable_lines = False
for order in self:
if order.state == 'sale' and order.is_rental_order:
rental_order_lines = order.order_line.filtered('is_rental')
order.has_pickable_lines = any(
sol.qty_delivered < sol.product_uom_qty for sol in rental_order_lines
)
order.has_returnable_lines = any(
sol.qty_returned < sol.qty_delivered for sol in rental_order_lines
)
@api.depends('is_rental_order', 'next_action_date', 'rental_status')
def _compute_is_late(self):
now = fields.Datetime.now()
for order in self:
tolerance_delay = relativedelta(hours=order.company_id.min_extra_hour)
order.is_late = (
order.is_rental_order
and order.rental_status in ['pickup', 'return'] # has_pickable_lines or has_returnable_lines
and order.next_action_date
and order.next_action_date + tolerance_delay < now
)
#=== ONCHANGE METHODS ===#
@api.onchange('rental_start_date', 'rental_return_date')
def _onchange_duration_show_update_duration(self):
self.show_update_duration = any(line.is_rental for line in self.order_line)
@api.onchange('is_rental_order')
def _onchange_is_rental_order(self):
self.ensure_one()
if self.is_rental_order:
self._rental_set_dates()
@api.onchange('rental_start_date')
def _onchange_rental_start_date(self):
self.order_line.filtered('is_rental')._compute_name()
@api.onchange('rental_return_date')
def _onchange_rental_return_date(self):
self.order_line.filtered('is_rental')._compute_name()
#=== ACTION METHODS ===#
def action_update_rental_prices(self):
self.ensure_one()
self._recompute_rental_prices()
self.message_post(body=_("Rental prices have been recomputed with the new period."))
def _recompute_rental_prices(self):
self.with_context(rental_recompute_price=True)._recompute_prices()
def _get_update_prices_lines(self):
""" Exclude non-rental lines from price recomputation"""
lines = super()._get_update_prices_lines()
if not self.env.context.get('rental_recompute_price'):
return lines
return lines.filtered('is_rental')
# PICKUP / RETURN : rental.processing wizard
def action_open_pickup(self):
self.ensure_one()
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
lines_to_pickup = self.order_line.filtered(
lambda r:
r.is_rental
and float_compare(r.product_uom_qty, r.qty_delivered, precision_digits=precision) > 0)
return self._open_rental_wizard('pickup', lines_to_pickup.ids)
def action_open_return(self):
self.ensure_one()
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
lines_to_return = self.order_line.filtered(
lambda r:
r.is_rental
and float_compare(r.qty_delivered, r.qty_returned, precision_digits=precision) > 0)
return self._open_rental_wizard('return', lines_to_return.ids)
def _open_rental_wizard(self, status, order_line_ids):
context = {
'order_line_ids': order_line_ids,
'default_status': status,
'default_order_id': self.id,
}
return {
'name': _('Validate a pickup') if status == 'pickup' else _('Validate a return'),
'view_mode': 'form',
'res_model': 'rental.order.wizard',
'type': 'ir.actions.act_window',
'target': 'new',
'context': context
}
def _get_portal_return_action(self):
""" Return the action used to display orders when returning from customer portal. """
if self.is_rental_order:
return self.env.ref('sale_renting.rental_order_action')
else:
return super()._get_portal_return_action()
def _get_product_catalog_domain(self):
""" Override of `_get_product_catalog_domain` to extend the domain to rental-only products.
:returns: A list of tuples that represents a domain.
:rtype: list
"""
domain = super()._get_product_catalog_domain()
if self.is_rental_order:
return expression.OR([
domain, [('rent_ok', '=', True), ('company_id', 'in', [self.company_id.id, False])]
])
return domain
#=== TOOLING ===#
def _rental_set_dates(self):
self.ensure_one()
if self.rental_start_date and self.rental_return_date:
return
start_date = fields.Datetime.now().replace(minute=0, second=0) + relativedelta(hours=1)
return_date = start_date + relativedelta(days=1)
self.update({
'rental_start_date': start_date,
'rental_return_date': return_date,
})
#=== BUSINESS METHODS ===#
def _get_product_catalog_order_data(self, products, **kwargs):
""" Override to add the rental dates for the price computation """
return super()._get_product_catalog_order_data(
products,
start_date=self.rental_start_date,
end_date=self.rental_return_date,
**kwargs,
)
def _update_order_line_info(self, product_id, quantity, **kwargs):
""" Override to add the context to mark the line as rental and the rental dates for the
price computation
"""
if self.is_rental_order:
self = self.with_context(in_rental_app=True)
product = self.env['product.product'].browse(product_id)
if product.rent_ok:
self._rental_set_dates()
return super()._update_order_line_info(
product_id,
quantity,
start_date=self.rental_start_date,
end_date=self.rental_return_date,
**kwargs,
)
def _get_action_add_from_catalog_extra_context(self):
""" Override to add rental dates in the context for product availabilities. """
extra_context = super()._get_action_add_from_catalog_extra_context()
extra_context.update(start_date=self.rental_start_date, end_date=self.rental_return_date)
return extra_context