mirror of
https://github.com/OCA/manufacture.git
synced 2025-01-28 16:37:15 +02:00
[11.0][REW] multi_level_mrp: substitute mrp forecast with stock demand estimates
This commit is contained in:
committed by
Jordi Ballester Alomar
parent
d613212ae7
commit
cad67a0231
@@ -14,11 +14,11 @@
|
||||
'mrp',
|
||||
'stock',
|
||||
'purchase',
|
||||
'stock_demand_estimate',
|
||||
],
|
||||
'data': [
|
||||
'security/multi_level_mrp_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'views/mrp_forecast_view.xml',
|
||||
'views/mrp_area_view.xml',
|
||||
'views/product_view.xml',
|
||||
'views/stock_location_view.xml',
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from . import mrp_area
|
||||
from . import stock_location
|
||||
from . import mrp_forecast
|
||||
from . import product
|
||||
from . import mrp_product
|
||||
from . import mrp_move
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
# © 2016 Ucamco - Wim Audenaert <wim.audenaert@ucamco.com>
|
||||
# © 2016 Eficent Business and IT Consulting Services S.L.
|
||||
# - Jordi Ballester Alomar <jordi.ballester@eficent.com>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from odoo import api, fields, models
|
||||
from datetime import date, datetime
|
||||
|
||||
|
||||
class MrpForecastForecast(models.Model):
|
||||
_name = 'mrp.forecast.forecast'
|
||||
_order = 'forecast_product_id, date'
|
||||
|
||||
date = fields.Date('Date')
|
||||
forecast_product_id = fields.Many2one('mrp.forecast.product', 'Product',
|
||||
select=True)
|
||||
name = fields.Char('Description')
|
||||
qty_forecast = fields.Float('Quantity')
|
||||
|
||||
|
||||
class MrpForecastProduct(models.Model):
|
||||
_name = 'mrp.forecast.product'
|
||||
# TODO: adapt to demand_estimate?? or at least to date_range??
|
||||
|
||||
@api.one
|
||||
@api.depends('product_id')
|
||||
def _function_name(self):
|
||||
if self.product_id:
|
||||
self.name = "[%s] %s" % (self.product_id.default_code,
|
||||
self.product_id.name, )
|
||||
|
||||
@api.one
|
||||
@api.depends('mrp_forecast_ids')
|
||||
def _function_forecast_m0(self):
|
||||
qty = 0.00
|
||||
for fc in self.mrp_forecast_ids:
|
||||
fcmonth = (datetime.date(datetime.strptime(
|
||||
fc.date, '%Y-%m-%d')).year * 100) + \
|
||||
datetime.date(datetime.strptime(
|
||||
fc.date, '%Y-%m-%d')).month
|
||||
tmonth = (date.today().year * 100) + date.today().month
|
||||
if not(datetime.date(datetime.strptime(
|
||||
fc.date, '%Y-%m-%d')) < date.today()) \
|
||||
and fcmonth == tmonth:
|
||||
qty += fc.qty_forecast
|
||||
self.qty_forecast_m0 = qty
|
||||
|
||||
@api.one
|
||||
@api.depends('mrp_forecast_ids')
|
||||
def _function_forecast_m1(self):
|
||||
qty = 0.00
|
||||
for fc in self.mrp_forecast_ids:
|
||||
fcmonth = (datetime.date(datetime.strptime(
|
||||
fc.date, '%Y-%m-%d')).year * 100) + datetime.date(
|
||||
datetime.strptime(fc.date, '%Y-%m-%d')).month
|
||||
tdyear = date.today().year
|
||||
tdmonth = date.today().month + 1
|
||||
if tdmonth > 12:
|
||||
tdmonth -= 12
|
||||
tdyear += 1
|
||||
tmonth = (tdyear * 100) + tdmonth
|
||||
if fcmonth == tmonth:
|
||||
qty += fc.qty_forecast
|
||||
self.qty_forecast_m1 = qty
|
||||
|
||||
@api.one
|
||||
@api.depends('mrp_forecast_ids')
|
||||
def _function_forecast_m2(self):
|
||||
qty = 0.00
|
||||
for fc in self.mrp_forecast_ids:
|
||||
fcmonth = (datetime.date(datetime.strptime(
|
||||
fc.date, '%Y-%m-%d')).year * 100) + datetime.date(
|
||||
datetime.strptime(fc.date, '%Y-%m-%d')).month
|
||||
tdyear = date.today().year
|
||||
tdmonth = date.today().month + 2
|
||||
if tdmonth > 12:
|
||||
tdmonth -= 12
|
||||
tdyear += 1
|
||||
tmonth = (tdyear * 100) + tdmonth
|
||||
if fcmonth == tmonth:
|
||||
qty += fc.qty_forecast
|
||||
self.qty_forecast_m2 = qty
|
||||
|
||||
@api.one
|
||||
@api.depends('mrp_forecast_ids')
|
||||
def _function_forecast_m3(self):
|
||||
qty = 0.00
|
||||
for fc in self.mrp_forecast_ids:
|
||||
fcmonth = (datetime.date(datetime.strptime(
|
||||
fc.date, '%Y-%m-%d')).year * 100) + datetime.date(
|
||||
datetime.strptime(fc.date, '%Y-%m-%d')).month
|
||||
tdyear = date.today().year
|
||||
tdmonth = date.today().month + 3
|
||||
if tdmonth > 12:
|
||||
tdmonth -= 12
|
||||
tdyear += 1
|
||||
tmonth = (tdyear * 100) + tdmonth
|
||||
if fcmonth == tmonth:
|
||||
qty += fc.qty_forecast
|
||||
self.qty_forecast_m3 = qty
|
||||
|
||||
@api.one
|
||||
@api.depends('mrp_forecast_ids')
|
||||
def _function_forecast_m4(self):
|
||||
qty = 0.00
|
||||
for fc in self.mrp_forecast_ids:
|
||||
fcmonth = (datetime.date(datetime.strptime(
|
||||
fc.date, '%Y-%m-%d')).year * 100) + datetime.date(
|
||||
datetime.strptime(fc.date, '%Y-%m-%d')).month
|
||||
tdyear = date.today().year
|
||||
tdmonth = date.today().month + 4
|
||||
if tdmonth > 12:
|
||||
tdmonth -= 12
|
||||
tdyear += 1
|
||||
tmonth = (tdyear * 100) + tdmonth
|
||||
if fcmonth == tmonth:
|
||||
qty += fc.qty_forecast
|
||||
self.qty_forecast_m4 = qty
|
||||
|
||||
@api.one
|
||||
@api.depends('mrp_forecast_ids')
|
||||
def _function_forecast_m5(self):
|
||||
qty = 0.00
|
||||
for fc in self.mrp_forecast_ids:
|
||||
fcmonth = (datetime.date(datetime.strptime(
|
||||
fc.date, '%Y-%m-%d')).year * 100) + datetime.date(
|
||||
datetime.strptime(fc.date, '%Y-%m-%d')).month
|
||||
tdyear = date.today().year
|
||||
tdmonth = date.today().month + 5
|
||||
if tdmonth > 12:
|
||||
tdmonth -= 12
|
||||
tdyear += 1
|
||||
tmonth = (tdyear * 100) + tdmonth
|
||||
if fcmonth == tmonth:
|
||||
qty += fc.qty_forecast
|
||||
self.qty_forecast_m5 = qty
|
||||
|
||||
@api.one
|
||||
@api.depends('mrp_forecast_ids')
|
||||
def _function_forecast_m6(self):
|
||||
qty = 0.00
|
||||
for fc in self.mrp_forecast_ids:
|
||||
fcmonth = (datetime.date(datetime.strptime(
|
||||
fc.date, '%Y-%m-%d')).year * 100) + datetime.date(
|
||||
datetime.strptime(fc.date, '%Y-%m-%d')).month
|
||||
tdyear = date.today().year
|
||||
tdmonth = date.today().month + 6
|
||||
if tdmonth > 12:
|
||||
tdmonth -= 12
|
||||
tdyear += 1
|
||||
tmonth = (tdyear * 100) + tdmonth
|
||||
if fcmonth == tmonth:
|
||||
qty += fc.qty_forecast
|
||||
self.qty_forecast_m6 = qty
|
||||
|
||||
@api.one
|
||||
@api.depends('mrp_forecast_ids')
|
||||
def _function_forecast_past(self):
|
||||
qty = 0.00
|
||||
for fc in self.mrp_forecast_ids:
|
||||
if datetime.date(
|
||||
datetime.strptime(fc.date, '%Y-%m-%d')) < date.today():
|
||||
qty += fc.qty_forecast
|
||||
self.qty_forecast_past = qty
|
||||
|
||||
@api.one
|
||||
@api.depends('mrp_forecast_ids')
|
||||
def _function_forecast_total(self):
|
||||
qty = 0.00
|
||||
for fc in self.mrp_forecast_ids:
|
||||
qty += fc.qty_forecast
|
||||
self.qty_forecast_total = qty
|
||||
|
||||
mrp_forecast_ids = fields.One2many('mrp.forecast.forecast',
|
||||
'forecast_product_id',
|
||||
'Forecast')
|
||||
name = fields.Char(compute='_function_name', string='Description')
|
||||
product_id = fields.Many2one('product.product', 'Product', index=True)
|
||||
mrp_area_id = fields.Many2one('mrp.area', 'MRP Area')
|
||||
qty_forecast_m0 = fields.Float(compute='_function_forecast_m0',
|
||||
string='This Month Forecast')
|
||||
qty_forecast_m1 = fields.Float(compute='_function_forecast_m1',
|
||||
string='Month+1 Forecast')
|
||||
qty_forecast_m2 = fields.Float(compute='_function_forecast_m2',
|
||||
string='Month+2 Forecast')
|
||||
qty_forecast_m3 = fields.Float(compute='_function_forecast_m3',
|
||||
string='Month+3 Forecast')
|
||||
qty_forecast_m4 = fields.Float(compute='_function_forecast_m4',
|
||||
string='Month+4 Forecast')
|
||||
qty_forecast_m5 = fields.Float(compute='_function_forecast_m5',
|
||||
string='Month+5 Forecast')
|
||||
qty_forecast_m6 = fields.Float(compute='_function_forecast_m6',
|
||||
string='Month+6 Forecast')
|
||||
qty_forecast_past = fields.Float(compute='_function_forecast_past',
|
||||
string='Past Forecast')
|
||||
qty_forecast_total = fields.Float(compute='_function_forecast_total',
|
||||
string='Total Forecast')
|
||||
@@ -6,6 +6,7 @@ from datetime import date, datetime, timedelta
|
||||
|
||||
from odoo.tests.common import SavepointCase
|
||||
from odoo import fields
|
||||
from dateutil.rrule import WEEKLY
|
||||
|
||||
|
||||
class TestMultiLevelMRP(SavepointCase):
|
||||
@@ -15,7 +16,10 @@ class TestMultiLevelMRP(SavepointCase):
|
||||
super(TestMultiLevelMRP, cls).setUpClass()
|
||||
cls.mo_obj = cls.env['mrp.production']
|
||||
cls.po_obj = cls.env['purchase.order']
|
||||
cls.product_obj = cls.env['product.product']
|
||||
cls.partner_obj = cls.env['res.partner']
|
||||
cls.stock_picking_obj = cls.env['stock.picking']
|
||||
cls.estimate_obj = cls.env['stock.demand.estimate']
|
||||
cls.multi_level_mrp_wiz = cls.env['multi.level.mrp']
|
||||
cls.mrp_inventory_procure_wiz = cls.env['mrp.inventory.procure']
|
||||
cls.mrp_inventory_obj = cls.env['mrp.inventory']
|
||||
@@ -34,6 +38,20 @@ class TestMultiLevelMRP(SavepointCase):
|
||||
cls.customer_location = cls.env.ref(
|
||||
'stock.stock_location_customers')
|
||||
|
||||
# Partner:
|
||||
vendor1 = cls.partner_obj.create({'name': 'Vendor 1'})
|
||||
|
||||
# Create products:
|
||||
route_buy = cls.env.ref('purchase.route_warehouse0_buy').id
|
||||
cls.prod_test = cls.product_obj.create({
|
||||
'name': 'Test Top Seller',
|
||||
'type': 'product',
|
||||
'list_price': 150.0,
|
||||
'produce_delay': 5.0,
|
||||
'route_ids': [(6, 0, [route_buy])],
|
||||
'seller_ids': [(0, 0, {'name': vendor1.id, 'price': 20.0})],
|
||||
})
|
||||
|
||||
# Create test picking:
|
||||
date_move = datetime.today() + timedelta(days=7)
|
||||
cls.picking_1 = cls.stock_picking_obj.create({
|
||||
@@ -90,7 +108,6 @@ class TestMultiLevelMRP(SavepointCase):
|
||||
'product_uom_id': cls.fp_2.uom_id.id,
|
||||
'date_planned_start': date_mo,
|
||||
})
|
||||
cls.multi_level_mrp_wiz.create({}).run_multi_level_mrp()
|
||||
|
||||
# Dates (Strings):
|
||||
today = datetime.today()
|
||||
@@ -102,6 +119,42 @@ class TestMultiLevelMRP(SavepointCase):
|
||||
cls.date_9 = fields.Date.to_string(today + timedelta(days=9))
|
||||
cls.date_10 = fields.Date.to_string(today + timedelta(days=10))
|
||||
|
||||
# Create Date Ranges:
|
||||
cls.dr_type = cls.env['date.range.type'].create({
|
||||
'name': 'Weeks',
|
||||
'company_id': False,
|
||||
'allow_overlap': False,
|
||||
})
|
||||
generator = cls.env['date.range.generator'].create({
|
||||
'date_start': today - timedelta(days=3),
|
||||
'name_prefix': 'W-',
|
||||
'type_id': cls.dr_type.id,
|
||||
'duration_count': 1,
|
||||
'unit_of_time': WEEKLY,
|
||||
'count': 3})
|
||||
generator.action_apply()
|
||||
|
||||
# Create Demand Estimates:
|
||||
ranges = cls.env['date.range'].search(
|
||||
[('type_id', '=', cls.dr_type.id)])
|
||||
qty = 140.0
|
||||
for dr in ranges:
|
||||
qty += 70.0
|
||||
cls._create_demand_estimate(
|
||||
cls.prod_test, cls.stock_location, dr, qty)
|
||||
|
||||
cls.multi_level_mrp_wiz.create({}).run_multi_level_mrp()
|
||||
|
||||
@classmethod
|
||||
def _create_demand_estimate(cls, product, location, date_range, qty):
|
||||
cls.estimate_obj.create({
|
||||
'product_id': product.id,
|
||||
'location_id': location.id,
|
||||
'product_uom': product.uom_id.id,
|
||||
'product_uom_qty': qty,
|
||||
'date_range_id': date_range.id,
|
||||
})
|
||||
|
||||
def test_01_mrp_levels(self):
|
||||
"""Tests computation of MRP levels."""
|
||||
self.assertEqual(self.fp_1.llc, 0)
|
||||
@@ -271,7 +324,25 @@ class TestMultiLevelMRP(SavepointCase):
|
||||
self.assertEqual(mrp_product.nbr_mrp_actions, 3)
|
||||
self.assertEqual(mrp_product.nbr_mrp_actions_4w, 3)
|
||||
|
||||
def test_06_procure_mo(self):
|
||||
def test_06_demand_estimates(self):
|
||||
"""Tests demand estimates integration."""
|
||||
estimates = self.estimate_obj.search(
|
||||
[('product_id', '=', self.prod_test.id)])
|
||||
self.assertEqual(len(estimates), 3)
|
||||
moves = self.mrp_move_obj.search([
|
||||
('product_id', '=', self.prod_test.id),
|
||||
])
|
||||
# 3 weeks - 3 days in the past = 18 days of valid estimates:
|
||||
moves_from_estimates = moves.filtered(lambda m: m.mrp_type == 'd')
|
||||
self.assertEqual(len(moves_from_estimates), 18)
|
||||
quantities = moves_from_estimates.mapped('mrp_qty')
|
||||
self.assertIn(-30.0, quantities) # 210 a week => 30.0 dayly:
|
||||
self.assertIn(-40.0, quantities) # 280 a week => 40.0 dayly:
|
||||
self.assertIn(-50.0, quantities) # 350 a week => 50.0 dayly:
|
||||
actions = moves.filtered(lambda m: m.mrp_action == 'po')
|
||||
self.assertEqual(len(actions), 18)
|
||||
|
||||
def test_07_procure_mo(self):
|
||||
"""Test procurement wizard with MOs."""
|
||||
mos = self.mo_obj.search([
|
||||
('product_id', '=', self.fp_1.id)])
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
<?xml version="1.0"?>
|
||||
<odoo>
|
||||
|
||||
<record model="ir.ui.view" id="mrp_forecast_tree">
|
||||
<field name="name">mrp.forecast.tree</field>
|
||||
<field name="model">mrp.forecast.product</field>
|
||||
<field name="type">form</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="MRP Product Forecast">
|
||||
<field name="mrp_area_id"/>
|
||||
<field name="product_id"/>
|
||||
<field name="qty_forecast_total"/>
|
||||
<field name="qty_forecast_past"/>
|
||||
<field name="qty_forecast_m0"/>
|
||||
<field name="qty_forecast_m1"/>
|
||||
<field name="qty_forecast_m2"/>
|
||||
<field name="qty_forecast_m3"/>
|
||||
<field name="qty_forecast_m4"/>
|
||||
<field name="qty_forecast_m5"/>
|
||||
<field name="qty_forecast_m6"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="mrp_forecast_form">
|
||||
<field name="name">mrp.forecast.form</field>
|
||||
<field name="model">mrp.forecast.product</field>
|
||||
<field name="type">form</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="MRP Product Forecast">
|
||||
<group colspan="4" col="2">
|
||||
<group>
|
||||
<field name="mrp_area_id"/>
|
||||
<field name="product_id"/>
|
||||
</group>
|
||||
</group>
|
||||
<group colspan="2" col="2">
|
||||
<separator string="Forecasts"/>
|
||||
<field name="mrp_forecast_ids" nolabel="1" colspan="2" context="{'default_forecast_product_id': active_id}">
|
||||
<tree string="Forecasts" editable="bottom">
|
||||
<field name="forecast_product_id" invisible="True"/>
|
||||
<field name="date"/>
|
||||
<field name="qty_forecast"/>
|
||||
</tree>
|
||||
</field>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.act_window" id="mrp_forecast_action">
|
||||
<field name="sequence" eval="20"/>
|
||||
<field name="name">MRP Forecast</field>
|
||||
<field name="res_model">mrp.forecast.product</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="view_id" ref="mrp_forecast_tree"/>
|
||||
<field name="act_window_id" ref="mrp_forecast_form"/>
|
||||
</record>
|
||||
<record model="ir.actions.act_window.view" id="mrp_forecast_form_action">
|
||||
<field name="sequence" eval="22"/>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="mrp_forecast_form"/>
|
||||
<field name="act_window_id" ref="mrp_forecast_action"/>
|
||||
</record>
|
||||
<record model="ir.actions.act_window.view" id="mrp_forecast_tree_action">
|
||||
<field name="sequence" eval="20"/>
|
||||
<field name="view_mode">tree</field>
|
||||
<field name="view_id" ref="mrp_forecast_tree"/>
|
||||
<field name="act_window_id" ref="mrp_forecast_action"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -30,10 +30,4 @@
|
||||
parent="menu_mrp_mrp"
|
||||
sequence="40"/>
|
||||
|
||||
<menuitem name="Forecasted Products"
|
||||
id="menu_mrp_forecast_products"
|
||||
action="mrp_forecast_action"
|
||||
parent="menu_mrp_mrp"
|
||||
sequence="60"/>
|
||||
|
||||
</odoo>
|
||||
|
||||
@@ -40,28 +40,24 @@ class MultiLevelMrp(models.TransientModel):
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _prepare_mrp_move_data_from_forecast(self, fc, fc_id, mrpproduct):
|
||||
def _prepare_mrp_move_data_from_forecast(
|
||||
self, estimate, mrp_product, date):
|
||||
mrp_type = 'd'
|
||||
origin = 'fc'
|
||||
mrp_date = date.today()
|
||||
if datetime.date(datetime.strptime(fc_id.date,
|
||||
'%Y-%m-%d')) > date.today():
|
||||
mrp_date = datetime.date(datetime.strptime(
|
||||
fc_id.date, '%Y-%m-%d'))
|
||||
return {
|
||||
'mrp_area_id': fc.mrp_area_id.id,
|
||||
'product_id': fc.product_id.id,
|
||||
'mrp_product_id': mrpproduct.id,
|
||||
'mrp_area_id': mrp_product.mrp_area_id.id,
|
||||
'product_id': mrp_product.product_id.id,
|
||||
'mrp_product_id': mrp_product.id,
|
||||
'production_id': None,
|
||||
'purchase_order_id': None,
|
||||
'purchase_line_id': None,
|
||||
'sale_order_id': None,
|
||||
'sale_line_id': None,
|
||||
'stock_move_id': None,
|
||||
'mrp_qty': -fc_id.qty_forecast,
|
||||
'current_qty': -fc_id.qty_forecast,
|
||||
'mrp_date': mrp_date,
|
||||
'current_date': mrp_date,
|
||||
'mrp_qty': -estimate.daily_qty,
|
||||
'current_qty': -estimate.daily_qty,
|
||||
'mrp_date': date,
|
||||
'current_date': date,
|
||||
'mrp_action': 'none',
|
||||
'mrp_type': mrp_type,
|
||||
'mrp_processed': False,
|
||||
@@ -379,13 +375,6 @@ class MultiLevelMrp(models.TransientModel):
|
||||
AND llc > 0;"""
|
||||
self.env.cr.execute(sql_stat)
|
||||
|
||||
sql_stat = """
|
||||
UPDATE product_product
|
||||
SET mrp_applicable=True
|
||||
FROM mrp_forecast_product
|
||||
WHERE product_product.id = mrp_forecast_product.product_id;"""
|
||||
self.env.cr.execute(sql_stat)
|
||||
|
||||
# self.env.cr.commit()
|
||||
counter = 0
|
||||
sql_stat = """
|
||||
@@ -408,15 +397,27 @@ class MultiLevelMrp(models.TransientModel):
|
||||
|
||||
@api.model
|
||||
def _init_mrp_move_from_forecast(self, mrp_product):
|
||||
forecast = self.env['mrp.forecast.product'].search(
|
||||
[('product_id', '=', mrp_product.product_id.id),
|
||||
('mrp_area_id', '=', mrp_product.mrp_area_id.id)])
|
||||
for fc in forecast:
|
||||
for fc_id in fc.mrp_forecast_ids:
|
||||
locations = self.env['stock.location'].search(
|
||||
[('id', 'child_of', mrp_product.mrp_area_id.location_id.id)])
|
||||
today = fields.Date.today()
|
||||
estimates = self.env['stock.demand.estimate'].search([
|
||||
('product_id', '=', mrp_product.product_id.id),
|
||||
('location_id', 'in', locations.ids),
|
||||
('date_range_id.date_end', '>=', today)
|
||||
])
|
||||
for rec in estimates:
|
||||
start = rec.date_range_id.date_start
|
||||
if start < today:
|
||||
start = today
|
||||
mrp_date = fields.Date.from_string(start)
|
||||
date_end = fields.Date.from_string(rec.date_range_id.date_end)
|
||||
delta = timedelta(days=1)
|
||||
while mrp_date <= date_end:
|
||||
mrp_move_data = \
|
||||
self._prepare_mrp_move_data_from_forecast(
|
||||
fc, fc_id, mrp_product)
|
||||
rec, mrp_product, mrp_date)
|
||||
self.env['mrp.move'].create(mrp_move_data)
|
||||
mrp_date += delta
|
||||
return True
|
||||
|
||||
# TODO: move this methods to mrp_product?? to be able to show moves with an action
|
||||
|
||||
Reference in New Issue
Block a user