diff --git a/stock_removal_location_by_priority/README.rst b/stock_removal_location_by_priority/README.rst new file mode 100644 index 000000000..9d50c17bb --- /dev/null +++ b/stock_removal_location_by_priority/README.rst @@ -0,0 +1,60 @@ +.. 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 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. + +Configuration +============= + +You can configure the removal priority as follows: + +#. Go to "Inventory > Configuration > Warehouse Management > Locations" +#. In each Location form, 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/9.0 + + +Bug Tracker +=========== + +Bugs are tracked on `GitHub 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. + + +Images +------ + +* Odoo Community Association: `Icon `_. + +Contributors +------------ + +* Miquel Raïch + + +Maintainer +---------- + +.. 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. diff --git a/stock_removal_location_by_priority/__init__.py b/stock_removal_location_by_priority/__init__.py new file mode 100644 index 000000000..08f93b3a4 --- /dev/null +++ b/stock_removal_location_by_priority/__init__.py @@ -0,0 +1,6 @@ +# -*- 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 diff --git a/stock_removal_location_by_priority/__openerp__.py b/stock_removal_location_by_priority/__openerp__.py new file mode 100644 index 000000000..87bddda80 --- /dev/null +++ b/stock_removal_location_by_priority/__openerp__.py @@ -0,0 +1,19 @@ +# -*- 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": "9.0.1.0.0", + "author": "Eficent, " + "Odoo Community Association (OCA)", + "website": "https://github.com/OCA/stock-logistics-warehouse", + "category": "Warehouse Management", + "depends": ["stock"], + "data": [ + 'views/stock_location_view.xml'], + "license": "AGPL-3", + 'installable': True, + 'application': False, +} diff --git a/stock_removal_location_by_priority/models/__init__.py b/stock_removal_location_by_priority/models/__init__.py new file mode 100644 index 000000000..89a4028f3 --- /dev/null +++ b/stock_removal_location_by_priority/models/__init__.py @@ -0,0 +1,7 @@ +# -*- 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_location +from . import stock_quant diff --git a/stock_removal_location_by_priority/models/stock_location.py b/stock_removal_location_by_priority/models/stock_location.py new file mode 100644 index 000000000..2c0a3c308 --- /dev/null +++ b/stock_removal_location_by_priority/models/stock_location.py @@ -0,0 +1,15 @@ +# -*- 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 openerp 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) diff --git a/stock_removal_location_by_priority/models/stock_quant.py b/stock_removal_location_by_priority/models/stock_quant.py new file mode 100644 index 000000000..72c6da995 --- /dev/null +++ b/stock_removal_location_by_priority/models/stock_quant.py @@ -0,0 +1,34 @@ +# -*- 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 openerp import api, fields, models +from openerp.tools.translate import _ +from openerp.exceptions import UserError + + +class StockQuant(models.Model): + _inherit = 'stock.quant' + + removal_priority = fields.Integer( + related='location_id.removal_priority', readonly=True, store=True) + + @api.model + def apply_removal_strategy(self, quantity, move, ops=False, + domain=None, removal_strategy='fifo'): + if any(move.mapped('location_id.removal_priority')): + if removal_strategy == 'fifo': + order = 'in_date, removal_priority, id' + return self._quants_get_order( + quantity, move, ops=ops, domain=domain, orderby=order) + elif removal_strategy == 'lifo': + order = 'in_date desc, removal_priority asc, id desc' + return self._quants_get_order( + quantity, move, ops=ops, domain=domain, orderby=order) + raise UserError(_('Removal strategy %s not implemented.') % ( + removal_strategy,)) + else: + return super(StockQuant, self).apply_removal_strategy( + self, quantity, move, ops=ops, domain=domain, + removal_strategy=removal_strategy) diff --git a/stock_removal_location_by_priority/static/description/icon.png b/stock_removal_location_by_priority/static/description/icon.png new file mode 100644 index 000000000..3a0328b51 Binary files /dev/null and b/stock_removal_location_by_priority/static/description/icon.png differ diff --git a/stock_removal_location_by_priority/tests/__init__.py b/stock_removal_location_by_priority/tests/__init__.py new file mode 100644 index 000000000..574b5131a --- /dev/null +++ b/stock_removal_location_by_priority/tests/__init__.py @@ -0,0 +1,6 @@ +# -*- 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 diff --git a/stock_removal_location_by_priority/tests/test_stock_removal_location_by_priority.py b/stock_removal_location_by_priority/tests/test_stock_removal_location_by_priority.py new file mode 100644 index 000000000..1c2a4f460 --- /dev/null +++ b/stock_removal_location_by_priority/tests/test_stock_removal_location_by_priority.py @@ -0,0 +1,140 @@ +# -*- 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 openerp.tests.common import TransactionCase + + +class TestStockRemovalLocationByPriority(TransactionCase): + + def setUp(self): + super(TestStockRemovalLocationByPriority, self).setUp() + self.res_users_model = self.env['res.users'] + self.stock_location_model = self.env['stock.location'] + 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.quant_model = self.env['stock.quant'] + + self.picking_internal = self.env.ref('stock.picking_type_internal') + self.picking_out = self.env.ref('stock.picking_type_out') + self.location_supplier = self.env.ref('stock.stock_location_suppliers') + + self.company = self.env.ref('base.main_company') + self.partner = self.env.ref('base.res_partner_1') + self.g_stock_user = self.env.ref('stock.group_stock_user') + + self.user = self._create_user( + 'user_1', [self.g_stock_user], self.company).id + + self.wh1 = self.stock_warehouse_model.create({ + 'name': 'WH1', + 'code': 'WH1', + }) + + # Create a locations: + self.stock = self.stock_location_model.create({ + 'name': 'Stock Base', + 'usage': 'internal', + }) + self.shelf_A = self.stock_location_model.create({ + 'name': 'Shelf_A', + 'usage': 'internal', + 'location_id': self.stock.id, + }) + self.shelf_B = self.stock_location_model.create({ + 'name': 'Shelf_B', + 'usage': 'internal', + 'location_id': self.stock.id, + 'removal_priority': 5, + }) + + # Create a product: + self.product_templ_1 = self.product_template_model.create({ + 'name': 'Test Product Template 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)] + }) + return user + + def _create_picking(self, picking_type, location, location_dest, qty): + + picking = self.stock_picking_model.sudo(self.user).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_uom_qty': qty, + 'location_id': location.id, + 'location_dest_id': location_dest.id, + 'price_unit': 2 + })] + }) + return picking + + def test_stock_removal_location_by_priority(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 + + picking_1 = self._create_picking( + self.picking_internal, self.stock, self.shelf_A, 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.') diff --git a/stock_removal_location_by_priority/views/stock_location_view.xml b/stock_removal_location_by_priority/views/stock_location_view.xml new file mode 100644 index 000000000..5a287231b --- /dev/null +++ b/stock_removal_location_by_priority/views/stock_location_view.xml @@ -0,0 +1,18 @@ + + + + + + + Location form - removal priority extension + stock.location + + + + + + + + +