mirror of
https://github.com/OCA/stock-logistics-warehouse.git
synced 2025-01-21 14:27:28 +02:00
[MIG] Migrate stock_mts_mto_rule to 12.0
Major changes to the way Warehouses update their routes/rules and self heal. Improved rule code to use `float_compare` and `float_is_zero`.
This commit is contained in:
@@ -47,7 +47,7 @@ You should not select both the mts+mto route and the mto route.
|
||||
|
||||
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
|
||||
:alt: Try me on Runbot
|
||||
:target: https://runbot.odoo-community.org/runbot/153/11.0
|
||||
:target: https://runbot.odoo-community.org/runbot/153/12.0
|
||||
|
||||
Known issues
|
||||
============
|
||||
@@ -73,6 +73,7 @@ Contributors
|
||||
------------
|
||||
|
||||
* Florian da Costa <florian.dacosta@akretion.com>
|
||||
* Jared Kipe <jared@hibou.io>
|
||||
|
||||
Do not contact contributors directly about support or help with technical issues.
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
from . import model
|
||||
from . import models
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
{'name': 'Stock MTS+MTO Rule',
|
||||
'version': '11.0.1.0.0',
|
||||
'author': 'Akretion,Odoo Community Association (OCA)',
|
||||
'website': 'http://www.akretion.com',
|
||||
'license': 'AGPL-3',
|
||||
'category': 'Warehouse',
|
||||
'summary': 'Add a MTS+MTO route',
|
||||
'depends': ['stock',
|
||||
],
|
||||
'demo': [],
|
||||
'data': ['data/stock_data.xml',
|
||||
'view/pull_rule.xml',
|
||||
'view/warehouse.xml',
|
||||
],
|
||||
'installable': True,
|
||||
}
|
||||
{
|
||||
'name': 'Stock MTS+MTO Rule',
|
||||
'summary': 'Add a MTS+MTO route',
|
||||
'version': '12.0.1.0.0',
|
||||
'development_status': 'Mature',
|
||||
'category': 'Warehouse',
|
||||
'website': 'https://github.com/OCA/stock-logistics-warehouse',
|
||||
'author': 'Akretion,Odoo Community Association (OCA)',
|
||||
'license': 'AGPL-3',
|
||||
'application': False,
|
||||
'installable': True,
|
||||
'depends': [
|
||||
'stock',
|
||||
],
|
||||
'data': [
|
||||
'data/stock_data.xml',
|
||||
'view/pull_rule.xml',
|
||||
'view/warehouse.xml',
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
from . import procurement_rule
|
||||
from . import stock_warehouse
|
||||
@@ -1,139 +0,0 @@
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import models, api, fields, exceptions
|
||||
from odoo.tools.translate import _
|
||||
|
||||
|
||||
class StockWarehouse(models.Model):
|
||||
_inherit = 'stock.warehouse'
|
||||
|
||||
mto_mts_management = fields.Boolean(
|
||||
'Use MTO+MTS rules',
|
||||
help='If this new route is selected on product form view, a '
|
||||
'purchase order will be created only if the virtual stock is '
|
||||
'less than 0 else, the product will be taken from stocks')
|
||||
mts_mto_rule_id = fields.Many2one('procurement.rule',
|
||||
'MTO+MTS rule')
|
||||
|
||||
@api.multi
|
||||
def _get_mts_mto_rule(self):
|
||||
self.ensure_one()
|
||||
route_model = self.env['stock.location.route']
|
||||
pull_model = self.env['procurement.rule']
|
||||
try:
|
||||
mts_mto_route = self.env.ref(
|
||||
'stock_mts_mto_rule.route_mto_mts')
|
||||
except:
|
||||
mts_mto_route = route_model.search([
|
||||
('name', 'like', 'Make To Order + Make To Stock')
|
||||
])
|
||||
if not mts_mto_route:
|
||||
raise exceptions.Warning(_(
|
||||
'Can\'t find any generic MTS+MTO route.'))
|
||||
|
||||
if not self.mto_pull_id:
|
||||
raise exceptions.Warning(_(
|
||||
'Can\'t find MTO Rule on the warehouse'))
|
||||
|
||||
mts_rules = pull_model.search(
|
||||
[('location_src_id', '=', self.lot_stock_id.id),
|
||||
('route_id', '=', self.delivery_route_id.id)])
|
||||
if not mts_rules:
|
||||
raise exceptions.Warning(_(
|
||||
'Can\'t find MTS Rule on the warehouse'))
|
||||
return {
|
||||
'name': self._format_routename(route_type='mts_mto'),
|
||||
'route_id': mts_mto_route.id,
|
||||
'action': 'split_procurement',
|
||||
'mto_rule_id': self.mto_pull_id.id,
|
||||
'mts_rule_id': mts_rules[0].id,
|
||||
'warehouse_id': self.id,
|
||||
'location_id': self.mto_pull_id.location_id.id,
|
||||
'picking_type_id': self.mto_pull_id.picking_type_id.id,
|
||||
}
|
||||
|
||||
def _get_mto_pull_rules_values(self, route_values):
|
||||
"""
|
||||
Prevent changing standard MTO rules' action from "move"
|
||||
"""
|
||||
pull_rules_list = super(StockWarehouse, self).\
|
||||
_get_mto_pull_rules_values(route_values)
|
||||
for pull_rule in pull_rules_list:
|
||||
pull_rule['action'] = 'move'
|
||||
|
||||
return pull_rules_list
|
||||
|
||||
@api.multi
|
||||
def create_routes(self):
|
||||
pull_model = self.env['procurement.rule']
|
||||
res = super(StockWarehouse, self).create_routes()
|
||||
if self.mto_mts_management:
|
||||
mts_mto_pull_vals = self._get_mts_mto_rule()
|
||||
mts_mto_pull = pull_model.create(mts_mto_pull_vals)
|
||||
res['mts_mto_rule_id'] = mts_mto_pull.id
|
||||
return res
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
pull_model = self.env['procurement.rule']
|
||||
if 'mto_mts_management' in vals:
|
||||
if vals.get("mto_mts_management"):
|
||||
for warehouse in self:
|
||||
if not warehouse.mts_mto_rule_id:
|
||||
rule_vals = warehouse._get_mts_mto_rule()
|
||||
mts_mto_pull = pull_model.create(rule_vals)
|
||||
vals['mts_mto_rule_id'] = mts_mto_pull.id
|
||||
else:
|
||||
for warehouse in self:
|
||||
if warehouse.mts_mto_rule_id:
|
||||
warehouse.mts_mto_rule_id.unlink()
|
||||
res = super(StockWarehouse, self).write(vals)
|
||||
if 'mto_mts_management' in vals:
|
||||
self.with_context({'active_test': False})._update_routes()
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def get_all_routes_for_wh(self):
|
||||
all_routes = super(StockWarehouse, self).get_all_routes_for_wh()
|
||||
|
||||
if self.mto_mts_management and self.mts_mto_rule_id.route_id:
|
||||
all_routes += self.mts_mto_rule_id.route_id
|
||||
|
||||
return all_routes
|
||||
|
||||
@api.multi
|
||||
def _update_name_and_code(self, name, code):
|
||||
res = super(StockWarehouse, self)._update_name_and_code(name, code)
|
||||
if not name:
|
||||
return res
|
||||
for warehouse in self.filtered('mts_mto_rule_id'):
|
||||
warehouse.mts_mto_rule_id.name = (
|
||||
warehouse.mts_mto_rule_id.name.replace(
|
||||
warehouse.name, name, 1,
|
||||
)
|
||||
)
|
||||
return res
|
||||
|
||||
def _get_route_name(self, route_type):
|
||||
names = {'mts_mto': _('MTS+MTO')}
|
||||
if route_type in names:
|
||||
return names[route_type]
|
||||
|
||||
return super(StockWarehouse, self)._get_route_name(route_type)
|
||||
|
||||
@api.multi
|
||||
def _update_routes(self):
|
||||
res = super(StockWarehouse, self)._update_routes()
|
||||
for warehouse in self:
|
||||
mts_mto_rule_id = warehouse.mts_mto_rule_id
|
||||
if warehouse.delivery_steps and mts_mto_rule_id:
|
||||
pull_model = self.env['procurement.rule']
|
||||
warehouse.mts_mto_rule_id.location_id = \
|
||||
warehouse.mto_pull_id.location_id
|
||||
mts_rules = pull_model.search([
|
||||
('location_src_id', '=', warehouse.lot_stock_id.id),
|
||||
('location_id', '=', warehouse.mto_pull_id.location_id.id),
|
||||
('route_id', '=', warehouse.delivery_route_id.id),
|
||||
])
|
||||
warehouse.mts_mto_rule_id.mts_rule_id = mts_rules[0].id
|
||||
return res
|
||||
2
stock_mts_mto_rule/models/__init__.py
Normal file
2
stock_mts_mto_rule/models/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from . import stock_rule
|
||||
from . import stock_warehouse
|
||||
@@ -1,18 +1,19 @@
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import models, api, fields, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tools import float_compare, float_is_zero
|
||||
|
||||
|
||||
class ProcurementRule(models.Model):
|
||||
_inherit = 'procurement.rule'
|
||||
class StockRule(models.Model):
|
||||
_inherit = 'stock.rule'
|
||||
|
||||
action = fields.Selection(
|
||||
selection_add=[('split_procurement', 'Choose between MTS and MTO')])
|
||||
mts_rule_id = fields.Many2one(
|
||||
'procurement.rule', string="MTS Rule")
|
||||
'stock.rule', string="MTS Rule")
|
||||
mto_rule_id = fields.Many2one(
|
||||
'procurement.rule', string="MTO Rule")
|
||||
'stock.rule', string="MTO Rule")
|
||||
|
||||
@api.constrains('action', 'mts_rule_id', 'mto_rule_id')
|
||||
def _check_mts_mto_rule(self):
|
||||
@@ -21,25 +22,27 @@ class ProcurementRule(models.Model):
|
||||
if not rule.mts_rule_id or not rule.mto_rule_id:
|
||||
msg = _('No MTS or MTO rule configured on procurement '
|
||||
'rule: %s!') % (rule.name, )
|
||||
raise UserError(msg)
|
||||
raise ValidationError(msg)
|
||||
if (rule.mts_rule_id.location_src_id.id !=
|
||||
rule.mto_rule_id.location_src_id.id):
|
||||
msg = _('Inconsistency between the source locations of '
|
||||
'the mts and mto rules linked to the procurement '
|
||||
'rule: %s! It should be the same.') % (rule.name,)
|
||||
raise UserError(msg)
|
||||
raise ValidationError(msg)
|
||||
|
||||
@api.multi
|
||||
def get_mto_qty_to_order(self, product, product_qty, product_uom, values):
|
||||
self.ensure_one()
|
||||
precision = self.env['decimal.precision']\
|
||||
.precision_get('Product Unit of Measure')
|
||||
src_location_id = self.mts_rule_id.location_src_id.id
|
||||
product_location = product.with_context(location=src_location_id)
|
||||
virtual_available = product_location.virtual_available
|
||||
qty_available = product.uom_id._compute_quantity(
|
||||
virtual_available, product_uom)
|
||||
|
||||
if qty_available > 0:
|
||||
if qty_available >= product_qty:
|
||||
if float_compare(qty_available, 0.0, precision_digits=precision) > 0:
|
||||
if float_compare(qty_available, product_qty,
|
||||
precision_digits=precision) >= 0:
|
||||
return 0.0
|
||||
else:
|
||||
return product_qty - qty_available
|
||||
@@ -47,15 +50,17 @@ class ProcurementRule(models.Model):
|
||||
|
||||
def _run_split_procurement(self, product_id, product_qty, product_uom,
|
||||
location_id, name, origin, values):
|
||||
|
||||
precision = self.env['decimal.precision']\
|
||||
.precision_get('Product Unit of Measure')
|
||||
needed_qty = self.get_mto_qty_to_order(product_id, product_qty,
|
||||
product_uom, values)
|
||||
|
||||
if needed_qty == 0.0:
|
||||
if float_is_zero(needed_qty, precision_digits=precision):
|
||||
getattr(self.mts_rule_id, '_run_%s' % self.mts_rule_id.action)(
|
||||
product_id, product_qty, product_uom, location_id, name,
|
||||
origin, values)
|
||||
elif needed_qty == product_qty:
|
||||
elif float_compare(needed_qty, product_qty,
|
||||
precision_digits=precision) == 0.0:
|
||||
getattr(self.mto_rule_id, '_run_%s' % self.mto_rule_id.action)(
|
||||
product_id, product_qty, product_uom, location_id, name,
|
||||
origin, values)
|
||||
93
stock_mts_mto_rule/models/stock_warehouse.py
Normal file
93
stock_mts_mto_rule/models/stock_warehouse.py
Normal file
@@ -0,0 +1,93 @@
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import fields, models, _
|
||||
|
||||
|
||||
class StockWarehouse(models.Model):
|
||||
_inherit = 'stock.warehouse'
|
||||
|
||||
mto_mts_management = fields.Boolean(
|
||||
'Use MTO+MTS rules',
|
||||
help='If this new route is selected on product form view, a '
|
||||
'purchase order will be created only if the virtual stock is '
|
||||
'less than 0 else, the product will be taken from stocks')
|
||||
mts_mto_rule_id = fields.Many2one('stock.rule',
|
||||
'MTO+MTS rule')
|
||||
|
||||
def _get_all_routes(self):
|
||||
routes = super(StockWarehouse, self)._get_all_routes()
|
||||
routes |= self.mapped('mts_mto_rule_id.route_id')
|
||||
return routes
|
||||
|
||||
def _update_name_and_code(self, new_name=False, new_code=False):
|
||||
res = super(StockWarehouse, self)._update_name_and_code(new_name,
|
||||
new_code)
|
||||
if not new_name:
|
||||
return res
|
||||
for warehouse in self.filtered('mts_mto_rule_id'):
|
||||
warehouse.mts_mto_rule_id.write({
|
||||
'name': warehouse.mts_mto_rule_id.name.replace(warehouse.name,
|
||||
new_name, 1),
|
||||
})
|
||||
return res
|
||||
|
||||
def _get_route_name(self, route_type):
|
||||
if route_type == 'mts_mto':
|
||||
return _('MTS+MTO')
|
||||
return super(StockWarehouse, self)._get_route_name(route_type)
|
||||
|
||||
def _get_global_route_rules_values(self):
|
||||
rule = self.get_rules_dict()[self.id][self.delivery_steps]
|
||||
rule = [r for r in rule if r.from_loc == self.lot_stock_id][0]
|
||||
location_id = rule.from_loc
|
||||
location_dest_id = rule.dest_loc
|
||||
picking_type_id = rule.picking_type
|
||||
res = super(StockWarehouse, self)._get_global_route_rules_values()
|
||||
res.update({
|
||||
'mts_mto_rule_id': {
|
||||
'depends': ['delivery_steps', 'mto_mts_management'],
|
||||
'create_values': {
|
||||
'action': 'pull',
|
||||
'procure_method': 'make_to_order',
|
||||
'company_id': self.company_id.id,
|
||||
'auto': 'manual',
|
||||
'propagate': True,
|
||||
'route_id': self._find_global_route(
|
||||
'stock_mts_mto_rule.route_mto_mts',
|
||||
_('Make To Order + Make To Stock')).id,
|
||||
},
|
||||
'update_values': {
|
||||
'active': self.mto_mts_management,
|
||||
'name': self._format_rulename(location_id,
|
||||
location_dest_id,
|
||||
'MTS+MTO'),
|
||||
'location_id': location_dest_id.id,
|
||||
'location_src_id': location_id.id,
|
||||
'picking_type_id': picking_type_id.id,
|
||||
}
|
||||
},
|
||||
})
|
||||
return res
|
||||
|
||||
def _create_or_update_global_routes_rules(self):
|
||||
res = super(StockWarehouse, self)\
|
||||
._create_or_update_global_routes_rules()
|
||||
|
||||
if (self.mto_mts_management and self.mts_mto_rule_id
|
||||
and self.mts_mto_rule_id.action != 'split_procurement'):
|
||||
# Cannot create or update with the 'split_procurement' action due
|
||||
# to constraint and the fact that the constrained rule_ids may
|
||||
# not exist during the initial (or really any) calls of
|
||||
# _get_global_route_rules_values
|
||||
rule = self.env['stock.rule'].search([
|
||||
('location_id', '=', self.mts_mto_rule_id.location_id.id),
|
||||
('location_src_id', '=',
|
||||
self.mts_mto_rule_id.location_src_id.id),
|
||||
('route_id', '=', self.delivery_route_id.id),
|
||||
], limit=1)
|
||||
self.mts_mto_rule_id.write({
|
||||
'action': 'split_procurement',
|
||||
'mts_rule_id': rule.id,
|
||||
'mto_rule_id': self.mto_pull_id.id,
|
||||
})
|
||||
return res
|
||||
@@ -1,2 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from . import test_mto_mts_route
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import exceptions
|
||||
@@ -79,7 +78,7 @@ class TestMtoMtsRoute(TransactionCase):
|
||||
moves[0].procure_method)
|
||||
|
||||
def test_mts_mto_rule_contrains(self):
|
||||
rule = self.env['procurement.rule'].search(
|
||||
rule = self.env['stock.rule'].search(
|
||||
[('action', '=', 'split_procurement')], limit=1)
|
||||
with self.assertRaises(exceptions.ValidationError):
|
||||
rule.write({'mts_rule_id': False})
|
||||
@@ -88,30 +87,32 @@ class TestMtoMtsRoute(TransactionCase):
|
||||
|
||||
def test_mts_mto_route_mto_removed(self):
|
||||
self.env.ref('stock_mts_mto_rule.route_mto_mts').unlink()
|
||||
self.warehouse.mts_mto_rule_id = False
|
||||
with self.assertRaises(exceptions.Warning):
|
||||
self.warehouse.mto_mts_management = True
|
||||
with self.assertRaises(exceptions.UserError):
|
||||
# mts_mto_rule_id is checked as a global rule
|
||||
self.warehouse.mts_mto_rule_id = False
|
||||
|
||||
def test_mts_mto_route_mts_removed(self):
|
||||
self.warehouse.mto_mts_management = True
|
||||
self.env['procurement.rule'].search([
|
||||
rules = self.env['stock.rule'].search([
|
||||
('location_src_id', '=', self.warehouse.lot_stock_id.id),
|
||||
('route_id', '=', self.warehouse.delivery_route_id.id),
|
||||
]).unlink()
|
||||
])
|
||||
self.env.cr.execute(
|
||||
'UPDATE stock_move SET rule_id = NULL WHERE rule_id IN %s',
|
||||
(tuple(rules.ids), ))
|
||||
self.warehouse.mts_mto_rule_id = False
|
||||
with self.assertRaises(exceptions.Warning):
|
||||
self.warehouse.mto_mts_management = True
|
||||
self.warehouse.mto_mts_management = True
|
||||
self.assertTrue(self.warehouse.mts_mto_rule_id)
|
||||
|
||||
def test_mts_mto_route_mto_no_mts_rule(self):
|
||||
self.warehouse.mts_mto_rule_id = False
|
||||
self.warehouse.mto_pull_id = False
|
||||
with self.assertRaises(exceptions.Warning):
|
||||
self.warehouse.mto_mts_management = True
|
||||
self.warehouse.mto_mts_management = True
|
||||
self.assertTrue(self.warehouse.mts_mto_rule_id)
|
||||
|
||||
def test_create_routes(self):
|
||||
rule_obj = self.env['procurement.rule']
|
||||
created_routes = self.warehouse.create_routes()
|
||||
mts_mto_route = rule_obj.browse(created_routes['mts_mto_rule_id'])
|
||||
self.warehouse._create_or_update_route()
|
||||
mts_mto_route = self.warehouse.mts_mto_rule_id
|
||||
self.assertEqual(mts_mto_route.warehouse_id, self.warehouse)
|
||||
self.assertEqual(
|
||||
mts_mto_route.location_id, self.warehouse.mto_pull_id.location_id)
|
||||
@@ -126,11 +127,10 @@ class TestMtoMtsRoute(TransactionCase):
|
||||
warehouse_rule = self.warehouse.mts_mto_rule_id
|
||||
self.assertTrue(self.warehouse.mts_mto_rule_id)
|
||||
self.warehouse.mto_mts_management = False
|
||||
self.assertFalse(warehouse_rule.exists())
|
||||
self.assertFalse(self.warehouse.mts_mto_rule_id)
|
||||
self.assertFalse(warehouse_rule.active)
|
||||
|
||||
def test_get_all_routes_for_wh(self):
|
||||
routes = self.warehouse.get_all_routes_for_wh()
|
||||
routes = self.warehouse._get_all_routes()
|
||||
self.assertTrue(self.warehouse.mts_mto_rule_id)
|
||||
self.assertTrue(self.warehouse.mts_mto_rule_id.route_id in routes)
|
||||
|
||||
@@ -146,7 +146,7 @@ class TestMtoMtsRoute(TransactionCase):
|
||||
super(TestMtoMtsRoute, self).setUp()
|
||||
self.move_obj = self.env['stock.move']
|
||||
self.warehouse = self.env.ref('stock.warehouse0')
|
||||
self.uom = self.env['product.uom'].browse(1)
|
||||
self.uom = self.env['uom.uom'].browse(1)
|
||||
self.warehouse.mto_mts_management = True
|
||||
self.customer_loc = self.env.ref('stock.stock_location_customers')
|
||||
self.product = self.env.ref('product.product_product_4')
|
||||
@@ -169,11 +169,11 @@ class TestMtoMtsRoute(TransactionCase):
|
||||
'location_id': self.env.ref('stock.stock_location_stock').id,
|
||||
'location_src_id': self.env.ref(
|
||||
'stock.stock_location_suppliers').id,
|
||||
'action': 'move',
|
||||
'action': 'pull',
|
||||
'warehouse_id': self.warehouse.id,
|
||||
'picking_type_id': self.env.ref('stock.picking_type_out').id,
|
||||
'name': 'dummy rule',
|
||||
'route_id': self.dummy_route.id,
|
||||
}
|
||||
self.dummy_rule = self.env['procurement.rule'].create(rule_vals)
|
||||
self.dummy_rule = self.env['stock.rule'].create(rule_vals)
|
||||
self.warehouse.write({'route_ids': [(4, self.dummy_route.id)]})
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_procurement_rule_form_mto_mto" model="ir.ui.view">
|
||||
<field name="name">procurement.rule.mts.mto</field>
|
||||
<field name="model">procurement.rule</field>
|
||||
<field name="inherit_id" ref="stock.view_procurement_rule_form" />
|
||||
<record id="stock_location_route_form_view_mto_mto" model="ir.ui.view">
|
||||
<field name="name">stock.location.route.form.mts.mto</field>
|
||||
<field name="model">stock.location.route</field>
|
||||
<field name="inherit_id" ref="stock.stock_location_route_form_view" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="action" position="after">
|
||||
<field name="mts_rule_id"
|
||||
|
||||
Reference in New Issue
Block a user