diff options
Diffstat (limited to 'addons/stock/report/report_stock_rule.py')
| -rw-r--r-- | addons/stock/report/report_stock_rule.py | 139 |
1 files changed, 139 insertions, 0 deletions
diff --git a/addons/stock/report/report_stock_rule.py b/addons/stock/report/report_stock_rule.py new file mode 100644 index 00000000..ad2fc264 --- /dev/null +++ b/addons/stock/report/report_stock_rule.py @@ -0,0 +1,139 @@ +# -*- 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_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.get_warehouse() == 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.get_warehouse() == 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 |
