140 lines
7.6 KiB
Python
140 lines
7.6 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||
|
|
||
|
from odoo import api, models, _
|
||
|
from odoo.exceptions import UserError
|
||
|
|
||
|
|
||
|
class ReportStockRule(models.AbstractModel):
|
||
|
_name = 'report.stock.report_stock_rule'
|
||
|
_description = 'Stock rule report'
|
||
|
|
||
|
@api.model
|
||
|
def _get_report_values(self, docids, data=None):
|
||
|
# Overriding data values here since used also in _get_routes.
|
||
|
data['product_id'] = data.get('product_id', docids)
|
||
|
data['warehouse_ids'] = data.get('warehouse_ids', [])
|
||
|
|
||
|
product = self.env['product.product'].browse(data['product_id'])
|
||
|
warehouses = self.env['stock.warehouse'].browse(data['warehouse_ids'])
|
||
|
|
||
|
routes = self._get_routes(data)
|
||
|
|
||
|
# Some routes don't have a warehouse_id but contain rules of different warehouses,
|
||
|
# we filter here the ones we want to display and build for each one a dict containing the rule,
|
||
|
# their source and destination location.
|
||
|
relevant_rules = routes.mapped('rule_ids').filtered(lambda r: not r.warehouse_id or r.warehouse_id in warehouses)
|
||
|
rules_and_loc = []
|
||
|
for rule in relevant_rules:
|
||
|
rules_and_loc.append(self._get_rule_loc(rule, product))
|
||
|
|
||
|
locations = self._sort_locations(rules_and_loc, warehouses)
|
||
|
reordering_rules = self.env['stock.warehouse.orderpoint'].search([('product_id', '=', product.id)])
|
||
|
locations |= reordering_rules.mapped('location_id').filtered(lambda l: l not in locations)
|
||
|
locations_names = locations.mapped('display_name')
|
||
|
# Here we handle reordering rules and putaway strategies by creating the header_lines dict. This dict is indexed
|
||
|
# by location_id and contains itself another dict with the relevant reordering rules and putaway strategies.
|
||
|
header_lines = {}
|
||
|
for location in locations:
|
||
|
# TODO: group the RR by location_id to avoid a filtered at each loop
|
||
|
rr = reordering_rules.filtered(lambda r: r.location_id.id == location.id)
|
||
|
putaways = product.putaway_rule_ids.filtered(lambda p: p.location_in_id.id == location.id)
|
||
|
if putaways or rr:
|
||
|
header_lines[location.id] = {'putaway': [], 'orderpoint': []}
|
||
|
for putaway in putaways:
|
||
|
header_lines[location.id]['putaway'].append(putaway)
|
||
|
for r in rr:
|
||
|
header_lines[location.id]['orderpoint'].append(r)
|
||
|
route_lines = []
|
||
|
colors = self._get_route_colors()
|
||
|
for color_index, route in enumerate(routes):
|
||
|
rules_to_display = route.rule_ids & relevant_rules
|
||
|
if rules_to_display:
|
||
|
route_color = colors[color_index % len(colors)]
|
||
|
color_index = color_index + 1
|
||
|
for rule in rules_to_display:
|
||
|
rule_loc = [r for r in rules_and_loc if r['rule'] == rule][0]
|
||
|
res = []
|
||
|
for x in range(len(locations_names)):
|
||
|
res.append([])
|
||
|
idx = locations_names.index(rule_loc['destination'].display_name)
|
||
|
tpl = (rule, 'destination', route_color, )
|
||
|
res[idx] = tpl
|
||
|
idx = locations_names.index(rule_loc['source'].display_name)
|
||
|
tpl = (rule, 'origin', route_color, )
|
||
|
res[idx] = tpl
|
||
|
route_lines.append(res)
|
||
|
return {
|
||
|
'docs': product,
|
||
|
'locations': locations,
|
||
|
'header_lines': header_lines,
|
||
|
'route_lines': route_lines,
|
||
|
}
|
||
|
|
||
|
@api.model
|
||
|
def _get_route_colors(self):
|
||
|
return ['#FFA500', '#800080', '#228B22', '#008B8B', '#4682B4', '#FF0000', '#32CD32']
|
||
|
|
||
|
@api.model
|
||
|
def _get_routes(self, data):
|
||
|
""" Extract the routes to display from the wizard's content.
|
||
|
"""
|
||
|
product = self.env['product.product'].browse(data['product_id'])
|
||
|
warehouse_ids = self.env['stock.warehouse'].browse(data['warehouse_ids'])
|
||
|
return product.route_ids | product.categ_id.total_route_ids | warehouse_ids.mapped('route_ids')
|
||
|
|
||
|
@api.model
|
||
|
def _get_rule_loc(self, rule, product):
|
||
|
rule.ensure_one()
|
||
|
return {'rule': rule, 'source': rule.location_src_id, 'destination': rule.location_dest_id}
|
||
|
|
||
|
@api.model
|
||
|
def _sort_locations(self, rules_and_loc, warehouses):
|
||
|
""" We order the locations by setting first the locations of type supplier and manufacture,
|
||
|
then we add the locations grouped by warehouse and we finish by the locations of type
|
||
|
customer and the ones that were not added by the sort.
|
||
|
"""
|
||
|
all_src = self.env['stock.location'].concat(*([r['source'] for r in rules_and_loc]))
|
||
|
all_dest = self.env['stock.location'].concat(*([r['destination'] for r in rules_and_loc]))
|
||
|
all_locations = all_src | all_dest
|
||
|
ordered_locations = self.env['stock.location']
|
||
|
locations = all_locations.filtered(lambda l: l.usage in ('supplier', 'production'))
|
||
|
for warehouse_id in warehouses:
|
||
|
all_warehouse_locations = all_locations.filtered(lambda l: l.warehouse_id == warehouse_id)
|
||
|
starting_rules = [d for d in rules_and_loc if d['source'] not in all_warehouse_locations]
|
||
|
if starting_rules:
|
||
|
start_locations = self.env['stock.location'].concat(*([r['destination'] for r in starting_rules]))
|
||
|
else:
|
||
|
starting_rules = [d for d in rules_and_loc if d['source'] not in all_dest]
|
||
|
start_locations = self.env['stock.location'].concat(*([r['source'] for r in starting_rules]))
|
||
|
used_rules = self.env['stock.rule']
|
||
|
locations |= self._sort_locations_by_warehouse(rules_and_loc, used_rules, start_locations, ordered_locations, warehouse_id)
|
||
|
if any(location not in locations for location in all_warehouse_locations):
|
||
|
remaining_locations = self.env['stock.location'].concat(*([r['source'] for r in rules_and_loc])).filtered(lambda l: l not in locations)
|
||
|
locations |= self._sort_locations_by_warehouse(rules_and_loc, used_rules, remaining_locations, ordered_locations, warehouse_id)
|
||
|
locations |= all_locations.filtered(lambda l: l.usage in ('customer'))
|
||
|
locations |= all_locations.filtered(lambda l: l not in locations)
|
||
|
return locations
|
||
|
|
||
|
@api.model
|
||
|
def _sort_locations_by_warehouse(self, rules_and_loc, used_rules, start_locations, ordered_locations, warehouse_id):
|
||
|
""" We order locations by putting first the locations that are not the destination of others and do it recursively.
|
||
|
"""
|
||
|
start_locations = start_locations.filtered(lambda l: l.warehouse_id == warehouse_id)
|
||
|
ordered_locations |= start_locations
|
||
|
rules_start = []
|
||
|
for rule in rules_and_loc:
|
||
|
if rule['source'] in start_locations:
|
||
|
rules_start.append(rule)
|
||
|
used_rules |= rule['rule']
|
||
|
if rules_start:
|
||
|
rules_start_dest_locations = self.env['stock.location'].concat(*([r['destination'] for r in rules_start]))
|
||
|
remaining_rules = self.env['stock.rule'].concat(*([r['rule'] for r in rules_and_loc])) - used_rules
|
||
|
remaining_rules_location = self.env['stock.location']
|
||
|
for r in rules_and_loc:
|
||
|
if r['rule'] in remaining_rules:
|
||
|
remaining_rules_location |= r['destination']
|
||
|
start_locations = rules_start_dest_locations - ordered_locations - remaining_rules_location
|
||
|
ordered_locations = self._sort_locations_by_warehouse(rules_and_loc, used_rules, start_locations, ordered_locations, warehouse_id)
|
||
|
return ordered_locations
|