[ADD] stock, sale_stock, purchase_stock, mrp: Routes 2D report

We are adding a new report available on products that displays all the
routes applicable on the products and dislay them as arrows between the
different locations.

We also display the putaway rules and reordering rules on locations
where they are applied.

TASK-46972
This commit is contained in:
Pierre Masereel
2018-08-10 11:25:01 +02:00
parent aae5b4a41e
commit 94a3919d09
27 changed files with 685 additions and 16 deletions
+1
View File
@@ -38,6 +38,7 @@
'report/mrp_report_views_main.xml',
'report/mrp_report_bom_structure.xml',
'report/mrp_production_templates.xml',
'report/report_stock_rule.xml',
],
'qweb': ['static/src/xml/mrp.xml'],
'demo': [
+2 -1
View File
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import mrp_report_bom_structure
from . import mrp_report_bom_structure
from . import report_stock_rule
+17
View File
@@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
class ReportStockRule(models.AbstractModel):
_inherit = 'report.stock.report_stock_rule'
@api.model
def _get_rule_loc(self, rule, product_id):
""" We override this method to handle manufacture rule which do not have a location_src_id.
"""
res = super(ReportStockRule, self)._get_rule_loc(rule, product_id)
if rule.action == 'manufacture':
res['source'] = product_id.property_stock_production or self.env.ref('stock.location_production')
return res
+32
View File
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<template id="mrp_report_stock_rule" inherit_id="stock.report_stock_rule">
<xpath expr="//div[hasclass('o_report_stock_rule_rule')]/t" position="before">
<t t-if="rule[0].action == 'manufacture'">
<t t-if="rule[1] == 'origin'">
<t t-call="stock.report_stock_rule_left_arrow"/>
</t>
</t>
</xpath>
<xpath expr="//div[hasclass('o_report_stock_rule_rule')]/t[last()]" position="after">
<t t-if="rule[0].action == 'manufacture'">
<t t-if="rule[1] == 'destination'">
<t t-call="stock.report_stock_rule_right_arrow"/>
</t>
</t>
</xpath>
<xpath expr="//div[hasclass('o_report_stock_rule_rule_name')]/span" position="before">
<t t-if="rule[0].action == 'manufacture'">
<i class="fa fa-wrench fa-fw" t-attf-style="color: #{color};"/>
</t>
</xpath>
<xpath expr="//div[hasclass('o_report_stock_rule_legend')]" position="inside">
<div class="o_report_stock_rule_legend_line">
<div class="o_report_stock_rule_legend_label">Manufacture</div>
<div class="o_report_stock_rule_legend_symbol">
<div class="fa fa-wrench fa-fw" t-attf-style="color: #{color};"/>
</div>
</div>
</xpath>
</template>
</odoo>
+1
View File
@@ -20,6 +20,7 @@
'views/stock_production_lot_views.xml',
'report/purchase_report_views.xml',
'report/purchase_report_templates.xml',
'report/report_stock_rule.xml',
],
'demo': [
'data/purchase_stock_demo.xml',
+1
View File
@@ -2,3 +2,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import purchase_report
from . import report_stock_rule
@@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
class ReportStockRule(models.AbstractModel):
_inherit = 'report.stock.report_stock_rule'
@api.model
def _get_rule_loc(self, rule, product_id):
""" We override this method to handle buy rules which do not have a location_src_id.
"""
res = super(ReportStockRule, self)._get_rule_loc(rule, product_id)
if rule.action == 'buy':
res['source'] = self.env.ref('stock.stock_location_suppliers')
return res
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<template id="purchase_report_stock_rule" inherit_id="stock.report_stock_rule">
<xpath expr="//div[hasclass('o_report_stock_rule_rule')]/t" position="before">
<t t-if="rule[0].action == 'buy'">
<t t-if="rule[1] == 'origin'">
<t t-call="stock.report_stock_rule_left_arrow"/>
</t>
</t>
</xpath>
<xpath expr="//div[hasclass('o_report_stock_rule_rule')]/t[last()]" position="after">
<t t-if="rule[0].action == 'buy'">
<t t-if="rule[1] == 'destination'">
<t t-call="stock.report_stock_rule_right_arrow"/>
</t>
</t>
</xpath>
<xpath expr="//div[hasclass('o_report_stock_rule_rule_name')]/span" position="before">
<t t-if="rule[0].action == 'buy'">
<i class="fa fa-shopping-cart fa-fw" t-attf-style="color: #{color};"/>
</t>
</xpath>
<xpath expr="//div[hasclass('o_report_stock_rule_legend')]" position="inside">
<div class="o_report_stock_rule_legend_line">
<div class="o_report_stock_rule_legend_label">Buy</div>
<div class="o_report_stock_rule_legend_symbol">
<div class="fa fa-shopping-cart fa-fw" t-attf-style="color: #{color};"/>
</div>
</div>
</xpath>
</template>
</odoo>
+1
View File
@@ -4,3 +4,4 @@
from . import controllers
from . import models
from . import report
from . import wizard
+1
View File
@@ -38,6 +38,7 @@ You can choose flexible invoicing methods:
'report/sale_order_report_templates.xml',
'report/stock_report_deliveryslip.xml',
'data/sale_stock_data.xml',
'wizard/stock_rules_report_views.xml',
],
'demo': ['data/sale_order_demo.xml'],
'installable': True,
+1
View File
@@ -2,3 +2,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import sale_report
from . import report_stock_rule
@@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
class ReportStockRule(models.AbstractModel):
_inherit = 'report.stock.report_stock_rule'
@api.model
def _get_routes(self, data):
res = super(ReportStockRule, self)._get_routes(data)
if data.get('so_route_ids'):
res = self.env['stock.location.route'].browse(data['so_route_ids']) | res
return res
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import stock_rules_report
@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class StockRulesReport(models.TransientModel):
_inherit = 'stock.rules.report'
so_route_ids = fields.Many2many('stock.location.route', string='Apply specific rotues',
domain="[('sale_selectable', '=', True)]", help="Choose to apply SO lines specific routes.")
def _prepare_report_data(self):
data = super(StockRulesReport, self)._prepare_report_data()
data['so_route_ids'] = self.so_route_ids.ids
return data
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_stock_rules_report_sale" model="ir.ui.view">
<field name="name">Stock Rules Report Sale</field>
<field name="model">stock.rules.report</field>
<field name="inherit_id" ref="stock.view_stock_rules_report"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='warehouse_ids']" position="after">
<field name="so_route_ids" widget="many2many_tags" groups="sale_stock.group_route_so_lines"/>
</xpath>
</field>
</record>
</odoo>
+2
View File
@@ -34,6 +34,7 @@
'report/report_stockpicking_operations.xml',
'report/report_deliveryslip.xml',
'report/report_stockinventory.xml',
'report/report_stock_rule.xml',
'wizard/stock_change_product_qty_views.xml',
'wizard/stock_picking_return_views.xml',
@@ -42,6 +43,7 @@
'wizard/stock_backorder_confirmation_views.xml',
'wizard/stock_overprocessed_transfer_views.xml',
'wizard/stock_quantity_history.xml',
'wizard/stock_rules_report_views.xml',
'wizard/stock_warn_insufficient_qty_views.xml',
'wizard/product_replenish_views.xml',
'wizard/stock_track_confirmation_views.xml',
-6
View File
@@ -526,12 +526,6 @@ class ProductTemplate(models.Model):
raise UserError(_("You can not change the type of a product that is currently reserved on a stock move. If you need to change the type, you should first unreserve the stock move."))
return super(ProductTemplate, self).write(vals)
def action_view_routes(self):
routes = self.mapped('route_ids') | self.mapped('categ_id').mapped('total_route_ids') | self.env['stock.location.route'].search([('warehouse_selectable', '=', True)])
action = self.env.ref('stock.action_routes_form').read()[0]
action['domain'] = [('id', 'in', routes.ids)]
return action
def action_update_quantity_on_hand(self):
default_product_id = self.env.context.get('default_product_id', self.product_variant_id.id)
if self.env.user.user_has_groups('stock.group_stock_multi_locations') or (self.env.user.user_has_groups('stock.group_production_lot') and self.tracking != 'none'):
+9 -3
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
from odoo import fields, models, api
class RemovalStrategy(models.Model):
@@ -26,16 +26,22 @@ class PutAwayStrategy(models.Model):
'Fixed Locations Per Product', domain=[('product_id', '!=', False)], copy=True)
def putaway_apply(self, product):
put_away = self._get_putaway_rule(product)
if put_away:
return put_away.fixed_location_id
return self.env['stock.location']
def _get_putaway_rule(self, product):
if self.product_location_ids:
put_away = self.product_location_ids.filtered(lambda x: x.product_id == product)
if put_away:
return put_away[0].fixed_location_id
return put_away[0]
if self.fixed_location_ids:
categ = product.categ_id
while categ:
put_away = self.fixed_location_ids.filtered(lambda x: x.category_id == categ)
if put_away:
return put_away[0].fixed_location_id
return put_away[0]
categ = categ.parent_id
return self.env['stock.location']
+1
View File
@@ -2,3 +2,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import report_stock_forecast
from . import report_stock_rule
+135
View File
@@ -0,0 +1,135 @@
# -*- 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'
@api.model
def _get_report_values(self, docids, data=None):
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)
if location.putaway_strategy_id or rr:
header_lines[location.id] = {'putaway': [], 'orderpoint': []}
if location.putaway_strategy_id:
putaway = location.putaway_strategy_id._get_putaway_rule(product)
header_lines[location.id]['putaway'].append(putaway)
if rr:
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
+169
View File
@@ -0,0 +1,169 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<template id="assets_common" name="report_stock_rule assets" inherit_id="web.report_assets_common">
<xpath expr="." position="inside">
<link rel="stylesheet" type="text/scss" href="/stock/static/src/scss/report_stock_rule.scss"/>
</xpath>
</template>
<template id="report_stock_rule">
<t t-set="data_report_landscape" t-value="True"/>
<t t-call="web.html_container">
<t t-foreach="docs" t-as="o">
<div class="article o_report_stock_rule">
<div class="page">
<h2 t-field="o.name"/>
<table class="table table-condensed table-bordered">
<thead>
<tr>
<t t-foreach="locations" t-as="location">
<th class="o_report_stock_rule_location_header">
<div t-att-res-id="location.id" t-att-res-model="location._name" view-type="form">
<t t-esc="location.display_name"/>
</div>
<t t-if="header_lines.get(location.id)">
<t t-foreach="header_lines[location.id]['putaway']" t-as="lines">
<t t-foreach="lines" t-as="line">
<div class="o_report_stock_rule_putaway" t-att-res-id="location.id" t-att-res-model="location._name" view-type="form">
<p>Putaway: <t t-esc="line.fixed_location_id.display_name"/></p>
</div>
</t>
</t>
<t t-foreach="header_lines[location.id]['orderpoint']" t-as="lines">
<t t-foreach="lines" t-as="line">
<div class="o_report_stock_rule_putaway" t-att-res-id="line.id" t-att-res-model="line._name" view-type="form">
<p>[<t t-esc="line.display_name"/>]<br/>min: <t t-esc="line.product_min_qty"/>, max:<t t-esc="line.product_max_qty"/></p>
</div>
</t>
</t>
</t>
</th>
</t>
</tr>
</thead>
<tbody>
<t t-foreach="route_lines" t-as="route_line">
<tr>
<t t-set="acc" t-value="0"/>
<t t-foreach="route_line" t-as="rule">
<t t-if="rule">
<t t-if="rule[0]._name == 'stock.rule'">
<t t-set="color" t-value="rule[2]"/>
<t t-if="acc > 0">
<t t-set="acc" t-value="acc+1"/>
<td t-att-colspan="acc" class="o_report_stock_rule_rule_cell">
<t t-set="padding" t-value="50.0/acc"/>
<div class="o_report_stock_rule_rule_main" t-att-res-id="rule[0].id" t-att-res-model="rule[0]._name" view-type="form" t-attf-title="#{rule[0].route_id.display_name}">
<div class="o_report_stock_rule_rule" t-attf-style="padding-left: #{padding}%; padding-right: #{padding}%;">
<t t-if="rule[1] == 'destination'">
<t t-if="rule[0].procure_method == 'make_to_order'">
<t t-call="stock.report_stock_rule_suspension_points"/>
</t>
<t t-if="rule[0].action in ('push', 'pull_push')">
<t t-call="stock.report_stock_rule_right_arrow"/>
</t>
</t>
<t t-if="rule[1] == 'origin' and rule[0].action in ('pull', 'pull_push')">
<t t-call="stock.report_stock_rule_left_arrow"/>
</t>
<t t-call="stock.report_stock_rule_line"/>
<t t-if="rule[1] == 'destination' and rule[0].action in ('pull', 'pull_push')">
<t t-call="stock.report_stock_rule_right_arrow"/>
</t>
<t t-if="rule[1] == 'origin'">
<t t-if="rule[0].action in ('push', 'pull_push')">
<t t-call="stock.report_stock_rule_left_arrow"/>
</t>
<t t-if="rule[0].procure_method == 'make_to_order'">
<t t-call="stock.report_stock_rule_suspension_points"/>
</t>
</t>
</div>
<div class="o_report_stock_rule_rule_name">
<span t-attf-style="color: #{color};"><t t-esc="rule[0].picking_type_id.name"/></span>
</div>
</div>
</td>
<t t-set="acc" t-value="0"/>
</t>
<t t-else="">
<t t-set="acc" t-value="acc+1"/>
</t>
</t>
</t>
<t t-else="">
<t t-if="acc > 0">
<t t-set="acc" t-value="acc+1"/>
</t>
<t t-if="acc == 0">
<td>
</td>
</t>
</t>
</t>
</tr>
</t>
</tbody>
</table>
<h3>Legend</h3>
<div class="o_report_stock_rule_legend">
<t t-set="color" t-value="'black'"/>
<div class="o_report_stock_rule_legend_line">
<div class="o_report_stock_rule_legend_label">Push Rule</div>
<div class="o_report_stock_rule_rule o_report_stock_rule_legend_symbol">
<t t-call="stock.report_stock_rule_right_arrow"/>
<t t-call="stock.report_stock_rule_line"/>
</div>
</div>
<div class="o_report_stock_rule_legend_line">
<div class="o_report_stock_rule_legend_label">Pull Rule</div>
<div class="o_report_stock_rule_rule o_report_stock_rule_legend_symbol">
<t t-call="stock.report_stock_rule_line"/>
<t t-call="stock.report_stock_rule_right_arrow"/>
</div>
</div>
<div class="o_report_stock_rule_legend_line">
<div class="o_report_stock_rule_legend_label">Trigger Another Rule</div>
<div class="o_report_stock_rule_rule o_report_stock_rule_legend_symbol">
<t t-call="stock.report_stock_rule_suspension_points"/>
<t t-call="stock.report_stock_rule_line"/>
</div>
</div>
</div>
</div>
</div>
</t>
</t>
</template>
<template id="report_stock_rule_line">
<div class="o_report_stock_rule_line">
<svg width="100%" height="100%" viewBox="0 0 100 10" preserveAspectRatio="none">
<line x1="0" y1="5" x2="100" y2="5" t-attf-style="stroke: #{color};"/>
</svg>
</div>
</template>
<template id="report_stock_rule_right_arrow">
<div class="o_report_stock_rule_arrow">
<svg width="100%" height="100%" viewBox="0 0 10 10">
<polygon points="0,0 0,10 10,5" t-attf-style="stroke: #{color}; fill: #{color};"/>
</svg>
</div>
</template>
<template id="report_stock_rule_left_arrow">
<div class="o_report_stock_rule_arrow">
<svg width="100%" height="100%" viewBox="0 0 10 10">
<polygon points="0,5 10,10 10,0" t-attf-style="stroke: #{color}; fill: #{color};"/>
</svg>
</div>
</template>
<template id="report_stock_rule_suspension_points">
<div class="o_report_stock_rule_arrow">
<svg width="100%" height="100%" viewBox="0 0 10 10" >
<line x1="1" y1="5" x2="4.5" y2="5" t-attf-style="stroke: #{color};"/>
<line x1="5.5" y1="5" x2="9" y2="5" t-attf-style="stroke: #{color};"/>
</svg>
</div>
</template>
</odoo>
@@ -60,5 +60,14 @@
file="stock.report_lot_label"
print_report_name="'Lot-Serial - %s' % object.name"
/>
<report
id="action_report_stock_rule"
model="product.template"
string="Product Routes Report"
report_type="qweb-html"
name="stock.report_stock_rule"
file="stock.report_stock_rule"
menu="False"
/>
</data>
</odoo>
@@ -0,0 +1,110 @@
.o_report_stock_rule{
.o_report_stock_rule_rule {
display: flex;
flex-flow: row nowrap;
}
.o_report_stock_rule_legend {
display: flex;
flex-flow: row wrap;
max-width: 1000px;
}
.o_report_stock_rule_legend_line {
flex: 0 1 auto;
display: flex;
flex-flow: row nowrap;
width: 29%;
margin-right: 20px;
margin-left: 20px;
margin-top: 15px;
min-width: 200px;
>.o_report_stock_rule_legend_label {
flex: 1 1 auto;
width: 30%;
min-width: 100px;
}
>.o_report_stock_rule_legend_symbol {
flex: 1 1 auto;
width: 70%;
}
}
.o_report_stock_rule_putaway {
>p {
text-align: center;
color: black;
font-weight: normal;
font-size: 12px
}
}
.o_report_stock_rule_line {
flex: 1 1 auto;
height: 20px;
>line {
stroke: black;
stroke-width: 1;
}
}
.o_report_stock_rule_arrow {
flex: 0 0 auto;
height: 20px;
width: 20px;
>svg {
>line {
stroke: black;
stroke-width: 1;
}
>polygon {
fill: black;
fill-opacity: 0.5;
stroke: black;
stroke-width: 1;
}
}
}
.o_report_stock_rule_rule_name {
text-align: center;
}
.o_report_stock_rule_symbol_cell {
border: none !important;
>div {
max-width: 200px;
height: 20px;
}
}
.o_report_stock_rule_rule_main {
height: 100%;
padding-top: 2px;
}
.o_report_stock_rule_location_header {
text-align: center;
>a {
display: block;
&:hover {
text-decoration: none;
cursor: pointer;
background-color: #efefef;
}
>div {
color: black;
}
}
}
.o_report_stock_rule_rule_cell {
padding:0 !important;
>a {
display: block;
&:hover {
text-decoration: none;
cursor: pointer;
background-color: #efefef;
}
}
}
}
+9 -6
View File
@@ -243,10 +243,12 @@
<span class="o_stat_value"><field name="reordering_max_qty"/></span>
</div>
</button>
<button string="Routes" type="object"
name="action_view_routes"
<button string="Routes" type="action"
name="%(action_stock_rules_report)d"
attrs="{'invisible':[('type', '!=', 'product')]}"
class="oe_stat_button" icon="fa-cogs" groups="base.group_no_one"/>
class="oe_stat_button" icon="fa-cogs"
context="{'default_product_id': id}"
groups="stock.group_stock_multi_locations"/>
<button string="Lot/Serial Number" type="object"
name="action_open_product_lot"
attrs="{'invisible': [('tracking', '=', 'none')]}"
@@ -324,11 +326,12 @@
<span class="o_stat_value"><field name="reordering_max_qty"/></span>
</div>
</button>
<button string="Routes" type="object"
name="action_view_routes"
<button string="Routes" type="action"
name="%(action_stock_rules_report)d"
attrs="{'invisible':[('type', '!=', 'product')]}"
class="oe_stat_button" icon="fa-cogs"
groups="base.group_no_one"/>
context="{'default_product_tmpl_id': id}"
groups="stock.group_stock_multi_locations"/>
<button string="Lot/Serial Number" type="object"
name="action_open_product_lot"
attrs="{'invisible': [('tracking', '=', 'none')]}"
+1
View File
@@ -8,6 +8,7 @@ from . import stock_immediate_transfer
from . import stock_backorder_confirmation
from . import stock_overprocessed_transfer
from . import stock_quantity_history
from . import stock_rules_report
from . import stock_warn_insufficient_qty
from . import product_replenish
from . import stock_track_confirmation
+48
View File
@@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class StockRulesReport(models.TransientModel):
_name = 'stock.rules.report'
product_id = fields.Many2one('product.product', string='Product', required=True)
product_tmpl_id = fields.Many2one('product.template', String='Product Template', required=True)
warehouse_ids = fields.Many2many('stock.warehouse', string='Warehouses', required=True,
help="Show the routes that apply on selected warehouses.")
product_has_variants = fields.Boolean('Has variants', default=False, required=True)
@api.model
def default_get(self, fields):
res = super(StockRulesReport, self).default_get(fields)
product_tmpl_id = False
if 'product_id' in fields:
if self.env.context.get('default_product_id'):
product_id = self.env['product.product'].browse(self.env.context['default_product_id'])
product_tmpl_id = product_id.product_tmpl_id
res['product_tmpl_id'] = product_id.product_tmpl_id.id
res['product_id'] = product_id.id
elif self.env.context.get('default_product_tmpl_id'):
product_tmpl_id = self.env['product.template'].browse(self.env.context['default_product_tmpl_id'])
res['product_tmpl_id'] = product_tmpl_id.id
res['product_id'] = product_tmpl_id.product_variant_id.id
if len(product_tmpl_id.product_variant_ids) > 1:
res['product_has_variants'] = True
if 'warehouse_ids' in fields:
warehouse_id = self.env['stock.warehouse'].search([], limit=1).id
res['warehouse_ids'] = [(6, 0, [warehouse_id])]
return res
def _prepare_report_data(self):
data = {
'product_id': self.product_id.id,
'warehouse_ids': self.warehouse_ids.ids,
}
return data
def print_report(self):
self.ensure_one()
data = self._prepare_report_data()
return self.env.ref('stock.action_report_stock_rule').report_action(None, data=data)
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_stock_rules_report" model="ir.ui.view">
<field name="name">Stock Rules Report</field>
<field name="model">stock.rules.report</field>
<field name="arch" type="xml">
<form string="Product Routes Report">
<group>
<field name="product_tmpl_id" invisible="1" />
<field name="product_has_variants" invisible="1" />
<field name="product_id"
domain="[('product_tmpl_id', '=', product_tmpl_id)]"
attrs="{'readonly': [('product_has_variants', '=', False)]}" options="{'no_create': True}"/>
<field name="warehouse_ids"
groups="stock.group_stock_multi_warehouses"
widget="many2many_tags" />
</group>
<footer>
<button name="print_report"
string="Print"
type="object"
class="btn-primary"/>
<button string="Cancel" class="btn-default" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="action_stock_rules_report" model="ir.actions.act_window">
<field name="name">Stock Rules Report</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">stock.rules.report</field>
<field name="view_type">form</field>
<field name="view_mode">form</field>
<field name="view_id" ref="view_stock_rules_report"/>
<field name="target">new</field>
</record>
</odoo>