[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:
@@ -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': [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
@@ -4,3 +4,4 @@
|
||||
from . import controllers
|
||||
from . import models
|
||||
from . import report
|
||||
from . import wizard
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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',
|
||||
|
||||
@@ -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'):
|
||||
|
||||
@@ -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']
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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')]}"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user