[11.0][MIG] stock_removal_location_by_priority

This commit is contained in:
Lois Rilo
2018-09-18 17:02:10 +02:00
committed by SergiCForgeFlow
parent f0354d1ca7
commit 54f2aab5f7
17 changed files with 211 additions and 220 deletions

View File

@@ -1,72 +1,97 @@
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
==================================
Stock Removal Location by Priority
==================================
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstock--logistics--warehouse-lightgray.png?logo=github
:target: https://github.com/OCA/stock-logistics-warehouse/tree/11.0/stock_removal_location_by_priority
:alt: OCA/stock-logistics-warehouse
.. |badge4| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
:target: https://runbot.odoo-community.org/runbot/153/11.0
:alt: Try me on Runbot
|badge1| |badge2| |badge3| |badge4|
This module adds a removal priority field on stock locations.
This priority applies when removing a product from different stock locations
and the incoming dates are equal in both locations.
**Table of contents**
.. contents::
:local:
Configuration
=============
You can configure the removal priority as follows:
You can activate the removal priority as follows:
#. Go to "Inventory > Configuration > Settings"
#. In 'Location & Warehouse' section, mark the "Removal Priority" option.
#. In 'Operations' section, mark the "Removal Priority" option.
#. You also need to activate the following settings in the section *Warehouse* if they are not yet:
NOTE: To be able to view this option you need to have already marked:
#. Manage several locations using *Storage Locations* option.
#. Advanced routing using "Multi-Step Routes" option.
#. Manage several locations in "Warehouses and Locations usage level" option.
#. Advanced routing of products using rules in "Routing" option.
Usage
=====
To use this module, you need to:
Then, set the *Removal Priority* in the desired locations. Remember that a
lower number means more priority:
#. Go to "Inventory > Configuration > Warehouse Management > Locations"
#. In each Location form, in the Logistics section, put a Removal Priority.
.. 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/10.0
Usage
=====
After configure your locations properly, the system will select the quant
at the location with more priority in case of equal date, no matter if you
use FIFO or LIFO removal strategy.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues
<https://github.com/OCA/stock-logistics-warehouse/issues>`_. In case of
trouble, please check there if your issue has already been reported. If you
spotted it first, help us smash it by providing detailed and welcomed feedback.
Bugs are tracked on `GitHub Issues <https://github.com/OCA/stock-logistics-warehouse/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed feedback.
Do not contact contributors directly about support or help with technical issues.
Images
------
Credits
=======
* Odoo Community Association: `Icon <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_.
Authors
~~~~~~~
* Eficent
Contributors
------------
~~~~~~~~~~~~
* Miquel Raïch <miquel.raich@eficent.com>
* Lois Rilo <lois.rilo@eficent.com>
Maintainers
~~~~~~~~~~~
Maintainer
----------
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
This module is maintained by the OCA.
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
To contribute to this module, please visit https://odoo-community.org.
This module is part of the `OCA/stock-logistics-warehouse <https://github.com/OCA/stock-logistics-warehouse/tree/11.0/stock_removal_location_by_priority>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

@@ -1,7 +1,2 @@
# -*- coding: utf-8 -*-
# Copyright 2016 Eficent Business and IT Consulting Services S.L.
# (http://www.eficent.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from . import models
from .init_hook import pre_init_hook

View File

@@ -1,11 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Eficent Business and IT Consulting Services S.L.
# (http://www.eficent.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
{
"name": "Stock Removal Location by Priority",
"summary": "Establish a removal priority on stock locations.",
"version": "10.0.1.0.0",
"version": "11.0.1.0.0",
"author": "Eficent, "
"Odoo Community Association (OCA)",
"website": "https://github.com/OCA/stock-logistics-warehouse",

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Eficent Business and IT Consulting Services, S.L.
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

View File

@@ -1,8 +1,3 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Eficent Business and IT Consulting Services S.L.
# (http://www.eficent.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from . import stock_config_settings
from . import res_config_settings
from . import stock_location
from . import stock_quant

View File

@@ -0,0 +1,17 @@
# Copyright 2017-18 Eficent Business and IT Consulting Services S.L.
# (http://www.eficent.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import fields, models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
group_removal_priority = fields.Boolean(
string="Removal Priority",
implied_group='stock_removal_location_by_priority.'
'group_removal_priority',
help="Removal priority that applies when the incoming dates "
"are equal in both locations.",
)

View File

@@ -1,17 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class StockConfigSettings(models.TransientModel):
_inherit = 'stock.config.settings'
group_removal_priority = fields.Selection([
(0, 'Don\'t use \'Removal Priority\' in Locations'),
(1, 'Use \'Removal Priority\' in Locations'),
], "Removal Priority",
implied_group='stock_removal_location_by_priority.'
'group_removal_priority',
help="Removal priority that applies when the incoming dates "
"are equal in both locations.")

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Eficent Business and IT Consulting Services S.L.
# (http://www.eficent.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
@@ -9,7 +8,8 @@ from odoo import fields, models
class StockLocation(models.Model):
_inherit = 'stock.location'
removal_priority = fields.Integer(help="This priority applies when "
"removing stock and incoming dates "
"are equal.",
string="Removal Priority", default=10)
removal_priority = fields.Integer(
string="Removal Priority", default=10,
help="This priority applies when removing stock and incoming dates "
"are equal.",
)

View File

@@ -1,29 +1,29 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Eficent Business and IT Consulting Services S.L.
# Copyright 2017-18 Eficent Business and IT Consulting Services S.L.
# (http://www.eficent.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import api, fields, models
from odoo.tools.translate import _
from odoo import api, fields, models, _
from odoo.exceptions import UserError
class Quant(models.Model):
class StockQuant(models.Model):
_inherit = 'stock.quant'
removal_priority = fields.Integer(
related='location_id.removal_priority', readonly=True, store=True)
related='location_id.removal_priority',
readonly=True, store=True,
)
@api.model
def _quants_removal_get_order(self, removal_strategy=None):
def _get_removal_strategy_order(self, removal_strategy=None):
if self.user_has_groups(
'stock_removal_location_by_priority.group_removal_priority'):
if removal_strategy == 'fifo':
return 'in_date, removal_priority, id'
return 'in_date ASC NULLS FIRST, removal_priority ASC, id'
elif removal_strategy == 'lifo':
return 'in_date desc, removal_priority asc, id desc'
return 'in_date DESC NULLS LAST, removal_priority ASC, id desc'
raise UserError(_('Removal strategy %s not implemented.') % (
removal_strategy,))
else:
return super(Quant, self)._quants_removal_get_order(
return super()._get_removal_strategy_order(
removal_strategy=removal_strategy)

View File

@@ -0,0 +1,14 @@
You can activate the removal priority as follows:
#. Go to "Inventory > Configuration > Settings"
#. In 'Operations' section, mark the "Removal Priority" option.
#. You also need to activate the following settings in the section *Warehouse* if they are not yet:
#. Manage several locations using *Storage Locations* option.
#. Advanced routing using "Multi-Step Routes" option.
Then, set the *Removal Priority* in the desired locations. Remember that a
lower number means more priority:
#. Go to "Inventory > Configuration > Warehouse Management > Locations"
#. In each Location form, in the Logistics section, put a Removal Priority.

View File

@@ -0,0 +1,2 @@
* Miquel Raïch <miquel.raich@eficent.com>
* Lois Rilo <lois.rilo@eficent.com>

View File

@@ -0,0 +1,3 @@
This module adds a removal priority field on stock locations.
This priority applies when removing a product from different stock locations
and the incoming dates are equal in both locations.

View File

@@ -0,0 +1,3 @@
After configure your locations properly, the system will select the quant
at the location with more priority in case of equal date, no matter if you
use FIFO or LIFO removal strategy.

View File

@@ -1,8 +1,8 @@
<odoo>
<data noupdate="0">
<record id="group_removal_priority" model="res.groups">
<field name="name">Removal Priority</field>
<field name="category_id" ref="base.module_category_hidden"/>
</record>
</data>
<odoo noupdate="0">
<record id="group_removal_priority" model="res.groups">
<field name="name">Removal Priority</field>
<field name="category_id" ref="base.module_category_hidden"/>
</record>
</odoo>

View File

@@ -1,6 +1 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Eficent Business and IT Consulting Services S.L.
# (http://www.eficent.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from . import test_stock_removal_location_by_priority

View File

@@ -1,7 +1,9 @@
# -*- coding: utf-8 -*-
# Copyright 2017 Eficent Business and IT Consulting Services S.L.
# Copyright 2017-18 Eficent Business and IT Consulting Services S.L.
# (http://www.eficent.com)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from datetime import date
from odoo.tests.common import TransactionCase
@@ -14,7 +16,7 @@ class TestStockRemovalLocationByPriority(TransactionCase):
self.stock_warehouse_model = self.env['stock.warehouse']
self.stock_picking_model = self.env['stock.picking']
self.stock_change_model = self.env['stock.change.product.qty']
self.product_template_model = self.env['product.template']
self.product_model = self.env['product.product']
self.quant_model = self.env['stock.quant']
self.picking_internal = self.env.ref('stock.picking_type_internal')
@@ -22,21 +24,24 @@ class TestStockRemovalLocationByPriority(TransactionCase):
self.location_supplier = self.env.ref('stock.stock_location_suppliers')
self.company = self.env.ref('base.main_company')
self.grp_rem_priority = self.env.ref(
grp_rem_priority = self.env.ref(
'stock_removal_location_by_priority.group_removal_priority')
self.g_stock_user = self.env.ref('stock.group_stock_user')
self.user = self._create_user(
'user_1', [self.g_stock_user, self.grp_rem_priority],
self.company).id
# We assign the group to admin, as the _get_removal_strategy_order
# method is going to be always executed as sudo.
user_admin = self.env.ref('base.user_root')
user_admin.groups_id = [(4, grp_rem_priority.id, 0)]
self.wh1 = self.stock_warehouse_model.create({
'name': 'WH1',
'code': 'WH1',
})
# Create a locations:
# Removal strategies:
self.fifo = self.env.ref('stock.removal_fifo')
self.lifo = self.env.ref('stock.removal_lifo')
# Create locations:
self.stock = self.stock_location_model.create({
'name': 'Stock Base',
'usage': 'internal',
@@ -45,6 +50,7 @@ class TestStockRemovalLocationByPriority(TransactionCase):
'name': 'Shelf_A',
'usage': 'internal',
'location_id': self.stock.id,
'removal_priority': 10,
})
self.shelf_B = self.stock_location_model.create({
'name': 'Shelf_B',
@@ -52,148 +58,93 @@ class TestStockRemovalLocationByPriority(TransactionCase):
'location_id': self.stock.id,
'removal_priority': 5,
})
self.stock_2 = self.stock_location_model.create({
'name': 'Another Stock Location',
'usage': 'internal',
})
# Create a product:
self.product_templ_1 = self.product_template_model.create({
'name': 'Test Product Template 1',
self.product_1 = self.product_model.create({
'name': 'Test Product 1',
'type': 'product',
'default_code': 'PROD_1',
})
def _create_user(self, login, groups, company):
group_ids = [group.id for group in groups]
user = self.res_users_model.create({
'name': login,
'login': login,
'password': 'demo',
'email': 'example@yourcompany.com',
'company_id': company.id,
'company_ids': [(4, company.id)],
'groups_id': [(6, 0, group_ids)]
# Create quants:
today = date.today()
q1 = self.quant_model.create({
'product_id': self.product_1.id,
'location_id': self.shelf_A.id,
'quantity': 5.0,
'in_date': today,
})
return user
q2 = self.quant_model.create({
'product_id': self.product_1.id,
'location_id': self.shelf_B.id,
'quantity': 5.0,
'in_date': today,
})
self.quants = q1 + q2
def _create_picking(self, picking_type, location, location_dest, qty):
picking = self.stock_picking_model.sudo(self.user).create({
picking = self.stock_picking_model.create({
'picking_type_id': picking_type.id,
'location_id': location.id,
'location_dest_id': location_dest.id,
'move_lines': [
(0, 0, {
'name': 'Test move',
'product_id': self.product1.id,
'product_uom': self.product1.uom_id.id,
'product_id': self.product_1.id,
'product_uom': self.product_1.uom_id.id,
'product_uom_qty': qty,
'location_id': location.id,
'location_dest_id': location_dest.id,
'price_unit': 2
'price_unit': 2,
})]
})
return picking
def test_stock_removal_location_by_priority_fifo(self):
"""Tests removal priority."""
wiz1 = self.stock_change_model.with_context(
active_id=self.product_templ_1.id,
active_model='product.template'
).create({'new_quantity': 20,
'location_id': self.stock.id,
'product_tmpl_id': self.product_templ_1.id,
})
wiz1.change_product_qty()
self.product1 = wiz1.product_id
def test_01_stock_removal_location_by_priority_fifo(self):
"""Tests removal priority with FIFO strategy."""
self.stock.removal_strategy_id = self.fifo
# quants must start unreserved
for q in self.quants:
self.assertEqual(q.reserved_quantity, 0.0)
if q.location_id == self.shelf_A:
self.assertEqual(q.removal_priority, 10)
if q.location_id == self.shelf_B:
self.assertEqual(q.removal_priority, 5)
self.assertEqual(self.quants[0].in_date, self.quants[1].in_date)
picking_1 = self._create_picking(
self.picking_internal, self.stock, self.shelf_A, 5)
self.picking_internal, self.stock, self.stock_2, 5)
picking_1.action_confirm()
picking_1.action_assign()
picking_2 = self._create_picking(
self.picking_internal, self.stock, self.shelf_B, 10)
picking_2.action_confirm()
picking_2.action_assign()
self.assertEqual(picking_1.pack_operation_ids.
linked_move_operation_ids.reserved_quant_id.in_date,
picking_2.pack_operation_ids.
linked_move_operation_ids.reserved_quant_id.in_date,
'Testing data not generated properly.')
wiz_act = picking_1.do_new_transfer()
wiz2 = self.env[wiz_act['res_model']].browse(wiz_act['res_id'])
wiz2.process()
wiz_act = picking_2.do_new_transfer()
wiz3 = self.env[wiz_act['res_model']].browse(wiz_act['res_id'])
wiz3.process()
picking_3 = self._create_picking(
self.picking_out, self.stock, self.location_supplier, 5)
picking_3.action_confirm()
picking_3.action_assign()
wiz_act = picking_3.do_new_transfer()
wiz4 = self.env[wiz_act['res_model']].browse(wiz_act['res_id'])
wiz4.process()
records = self.quant_model.search(
[('product_id', '=', self.product1.id)])
for record in records:
self.assertEqual(record.qty, 5,
'Removal_priority did\'nt work properly.')
def test_stock_removal_location_by_priority_lifo(self):
"""Tests removal priority."""
removal_method_id = self.env['product.removal'].search(
[('name', '=', 'lifo')]).id
self.stock.removal_strategy_id = removal_method_id
self.shelf_A.removal_strategy_id = removal_method_id
self.shelf_B.removal_strategy_id = removal_method_id
self.location_supplier.removal_strategy_id = removal_method_id
wiz1 = self.stock_change_model.with_context(
active_id=self.product_templ_1.id,
active_model='product.template'
).create({'new_quantity': 20,
'location_id': self.stock.id,
'product_tmpl_id': self.product_templ_1.id,
})
wiz1.change_product_qty()
self.product1 = wiz1.product_id
# quants must be reserved in Shelf B (lower removal_priority value).
for q in self.quants:
if q.location_id == self.shelf_A:
self.assertEqual(q.reserved_quantity, 0.0)
if q.location_id == self.shelf_B:
self.assertEqual(q.reserved_quantity, 5.0)
def test_02_stock_removal_location_by_priority_lifo(self):
"""Tests removal priority with LIFO strategy."""
self.stock.removal_strategy_id = self.lifo
# quants must start unreserved
for q in self.quants:
self.assertEqual(q.reserved_quantity, 0.0)
if q.location_id == self.shelf_A:
self.assertEqual(q.removal_priority, 10)
if q.location_id == self.shelf_B:
self.assertEqual(q.removal_priority, 5)
self.assertEqual(self.quants[0].in_date, self.quants[1].in_date)
picking_1 = self._create_picking(
self.picking_internal, self.stock, self.shelf_A, 5)
self.picking_internal, self.stock, self.stock_2, 5)
picking_1.action_confirm()
picking_1.action_assign()
picking_2 = self._create_picking(
self.picking_internal, self.stock, self.shelf_B, 10)
picking_2.action_confirm()
picking_2.action_assign()
self.assertEqual(picking_1.pack_operation_ids.
linked_move_operation_ids.reserved_quant_id.in_date,
picking_2.pack_operation_ids.
linked_move_operation_ids.reserved_quant_id.in_date,
'Testing data not generated properly.')
wiz_act = picking_1.do_new_transfer()
wiz2 = self.env[wiz_act['res_model']].browse(wiz_act['res_id'])
wiz2.process()
wiz_act = picking_2.do_new_transfer()
wiz3 = self.env[wiz_act['res_model']].browse(wiz_act['res_id'])
wiz3.process()
picking_3 = self._create_picking(
self.picking_out, self.stock, self.location_supplier, 5)
picking_3.action_confirm()
picking_3.action_assign()
wiz_act = picking_3.do_new_transfer()
wiz4 = self.env[wiz_act['res_model']].browse(wiz_act['res_id'])
wiz4.process()
records = self.quant_model.search(
[('product_id', '=', self.product1.id)])
for record in records:
self.assertEqual(record.qty, 5,
'Removal_priority did\'nt work properly.')
# quants must be reserved in Shelf B (lower removal_priority value).
for q in self.quants:
if q.location_id == self.shelf_A:
self.assertEqual(q.reserved_quantity, 0.0)
if q.location_id == self.shelf_B:
self.assertEqual(q.reserved_quantity, 5.0)

View File

@@ -1,16 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2017 Eficent Business and IT Consulting Services S.L.
<!-- Copyright 2017-18 Eficent Business and IT Consulting Services S.L.
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="view_stock_config_settings" model="ir.ui.view">
<field name="name">stock.config.view - removal_priority</field>
<field name="model">stock.config.settings</field>
<field name="inherit_id" ref="stock.view_stock_config_settings"/>
<field name="name">res.config.settings - removal_priority</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="stock.res_config_settings_view_form"/>
<field name="arch" type="xml">
<field name="group_warning_stock" position="after">
<field name="group_removal_priority" widget="radio"/>
</field>
<xpath expr="//div[@id='warning_info']" position="after">
<div class="col-xs-12 col-md-6 o_setting_box" id="removal_priority">
<div class="o_setting_left_pane">
<field name="group_removal_priority"/>
</div>
<div class="o_setting_right_pane">
<label for="group_removal_priority"/>
<div class="text-muted">
Use <i>Removal Priority</i> in Locations
</div>
</div>
</div>
</xpath>
</field>
</record>