Files

191 lines
8.6 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
from pytz import utc
from random import shuffle
from odoo import models
from odoo.exceptions import ValidationError
from odoo.tools import float_compare, float_round
class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'
def _compute_planning_hours_to_plan(self):
planning_rental_sols = self.filtered(
lambda sol:
sol.is_rental
and sol.state not in ['draft', 'sent']
and sol.product_id.planning_enabled
)
for line in planning_rental_sols:
if line.planning_slot_ids.resource_id.calendar_id:
days_per_week = line.company_id.resource_calendar_id.get_work_duration_data(line.start_date, line.return_date)['days']
else:
days_per_week = line.order_id.duration_days + (1 if line.order_id.remaining_hours else 0)
line.planning_hours_to_plan = line.company_id.resource_calendar_id.hours_per_day * days_per_week
super(SaleOrderLine, self - planning_rental_sols)._compute_planning_hours_to_plan()
def _is_planning_rental_service(self):
self.ensure_one()
return bool(
self.is_rental
and self.product_id.planning_enabled
and self.product_id.planning_role_id
)
def _get_planning_resources_available(self):
self.ensure_one()
if not self._is_planning_rental_service():
return self.env['resource.resource']
available_resources = self.product_id.planning_role_id.resource_ids
if not available_resources:
return available_resources
unavailable_resource_slots = self.env['planning.slot'].search([
('resource_id', 'in', available_resources.ids),
('start_datetime', '<=', self.return_date),
('end_datetime', '>=', self.start_date),
('sale_line_id', '!=', self.id),
])
resource_leaves = self.env['resource.calendar.leaves'].search([
('resource_id', 'in', available_resources.ids + [False]),
('date_from', '<=', self.return_date),
('date_to', '>=', self.start_date),
('company_id', '=', self.company_id.id),
])
removed_calendar_ids = self.env['resource.calendar']
unavailable_resources = unavailable_resource_slots.resource_id
for leave in resource_leaves:
if leave.resource_id:
unavailable_resources += leave.resource_id
# if no resource_id then leave applies to all resources with the same calendar_id.
elif leave.calendar_id:
removed_calendar_ids += leave.calendar_id
# if no calendar_id then leave applies to all resources.
else:
return self.env['resource.resource']
available_resources = available_resources.filtered(
lambda r: r not in unavailable_resources and r.calendar_id not in removed_calendar_ids
)
if not available_resources:
return available_resources
date_from = utc.localize(self.start_date)
date_to = utc.localize(self.return_date)
work_intervals_per_resource, _dummy = available_resources._get_valid_work_intervals(
date_from, date_to, available_resources.calendar_id
)
return available_resources.filtered(
lambda r: not r.calendar_id or work_intervals_per_resource[r.id]
)
def _planning_slot_vals_list_per_sol(self):
vals_list_per_sol = super()._planning_slot_vals_list_per_sol()
assigned_resource_ids = []
problematic_services = []
unit_uom = self.env.ref('uom.product_uom_unit')
for sol, vals_list in vals_list_per_sol.items():
if not sol._is_planning_rental_service():
continue
sync_shift_rental = sol.product_id.planning_role_id.sync_shift_rental
available_resources = sol._get_planning_resources_available().filtered(
lambda r: r.id not in assigned_resource_ids
)
if not available_resources and sync_shift_rental:
problematic_services.append(sol.product_id.name)
continue
free_resource_ids = available_resources.ids
flexible_resource_ids = available_resources.filtered(lambda r: not r.calendar_id).ids
shuffle(free_resource_ids)
# FIXME: check why it is needed
if free_resource_ids and free_resource_ids[0] not in flexible_resource_ids:
days_per_week = sol.company_id.resource_calendar_id.get_work_duration_data(sol.start_date, sol.return_date)['days']
sol.planning_hours_to_plan = sol.company_id.resource_calendar_id.hours_per_day * days_per_week
resource_id = False
if free_resource_ids and len(vals_list) <= len(free_resource_ids):
for index, vals in enumerate(vals_list):
resource_id = free_resource_ids[index]
vals['resource_id'] = resource_id
assigned_resource_ids.append(resource_id)
if sol.product_uom_id == unit_uom and float_compare(sol.product_uom_qty, 1, precision_rounding=sol.product_uom_id.rounding) > 0:
nb_shifts_to_generate = int(float_round(sol.product_uom_qty, 0, rounding_method="UP"))
if len(free_resource_ids) >= nb_shifts_to_generate:
vals_list.extend([
{**sol._planning_slot_values(), 'resource_id': free_resource_ids[i]}
for i in range(1, nb_shifts_to_generate)
])
elif sync_shift_rental:
raise ValidationError(
self.env._(
"This Sales Order can't be confirmed. No enough resources are available for the shifts in: %(product_name)s.",
product_name=sol.product_id.name,
)
)
elif sync_shift_rental:
problematic_services.append(sol.product_id.name)
if problematic_services:
raise ValidationError(
self.env._(
"This Sales Order can't be confirmed. No resources are available for the shifts in: %(problematic_services)s.",
problematic_services=problematic_services,
)
)
return vals_list_per_sol
def _planning_slot_values(self):
vals = super()._planning_slot_values()
if self.is_rental:
vals.update(
start_datetime=self.start_date,
end_datetime=self.return_date,
state='published',
)
return vals
def write(self, vals):
if 'product_uom_qty' in vals and (rental_sols := self.filtered(lambda sol: sol.is_rental and sol._should_generate_planning_slot())):
new_qty = int(vals['product_uom_qty'])
uom_hour = self.env.ref('uom.product_uom_hour')
for sol in rental_sols:
# ignore slot update for hours unless all slots are being unlinked
if new_qty > 0 and sol.product_id.uom_id == uom_hour:
continue
slots = sol.planning_slot_ids
slot_count = len(slots)
if new_qty < slot_count and not self.env.context.get("unlink_qty", False):
slots[new_qty:].unlink()
elif new_qty > slot_count:
self.env['planning.slot'].create([{
'sale_line_id': sol.id,
'start_datetime': sol.start_date,
'end_datetime': sol.return_date,
} for _ in range(new_qty - slot_count)])._set_slot_resource()
return super().write(vals)
def unlink(self):
rental_order_lines = self.filtered('is_rental')
if slots := self.env['planning.slot'].search([('sale_line_id', 'in', rental_order_lines.ids)]):
slots.unlink()
return super().unlink()
def update_product_uom_qty(self, planning_slots):
self.ensure_one()
if not self.order_is_rental:
return
uom_hour = self.env.ref('uom.product_uom_hour')
new_qty = 0
if self.product_uom_id == uom_hour:
for slot in planning_slots:
new_qty += slot.allocated_hours
else:
new_qty = len(planning_slots)
if self.product_uom_qty != new_qty:
self.with_context(unlink_qty=True).write({'product_uom_qty': new_qty})