[IMP] stock, mrp: picking before and after manufacturing

Currently MRP do not handle multi locations for components. In order to
be able to use both feature at the same time, we could:
- Improve mrp in order to handle multiple stock.move.line for a
componenet stock.move
- Add a rule that allow to bring all the good from multiple location to
a single location. (was already possible but it requires some
configuration)
This commit introduce a checkbox on the warehouse in order to
automatically configure all the routes/rules/locations/picking_types,...
necessary to bring all the components to a single location before
running the manufacturing order.

Technically it also refactor some stock_warehouse.py methods in order to
easily override the picking_type, rules and routes creation.

Thank to Hetashree Chauhan <hch@odoo.com> for his help.

task_id: 27785
This commit is contained in:
Arnold Moyaux
2018-08-13 15:38:34 +02:00
parent 177981b09f
commit 1e160dffd3
24 changed files with 955 additions and 408 deletions
+12
View File
@@ -4,3 +4,15 @@
from . import models
from . import wizard
from . import report
from odoo import api, SUPERUSER_ID
def _create_warehouse_data(cr, registry):
""" This hook is used to add a default manufacture_pull_id, manufacture
picking_type on every warehouse. It is necessary if the mrp module is
installed after some warehouses were already created.
"""
env = api.Environment(cr, SUPERUSER_ID, {})
warehouse_ids = env['stock.warehouse'].search([('manufacture_pull_id', '=', False)])
for warehouse_id in warehouse_ids:
warehouse_id.write({'manufacture_to_resupply': True})
+1
View File
@@ -46,4 +46,5 @@
],
'test': [],
'application': True,
'post_init_hook': '_create_warehouse_data',
}
-8
View File
@@ -83,14 +83,6 @@
<field name="sequence">0</field>
</record>
<function model="ir.model.data" name="_update_xmlids">
<value model="base" eval="[{
'xml_id': 'mrp.picking_type_manufacturing',
'record': obj().env.ref('stock.warehouse0').manu_type_id,
'noupdate': True,
}]"/>
</function>
</data>
</odoo>
+29 -3
View File
@@ -164,6 +164,31 @@ class MrpProduction(models.Model):
is_locked = fields.Boolean('Is Locked', default=True, copy=False)
show_final_lots = fields.Boolean('Show Final Lots', compute='_compute_show_lots')
production_location_id = fields.Many2one('stock.location', "Production Location", related='product_id.property_stock_production')
picking_ids = fields.Many2many('stock.picking', compute='_compute_picking_ids', string='Picking associated to this manufacturing order')
delivery_count = fields.Integer(string='Delivery Orders', compute='_compute_picking_ids')
@api.depends('procurement_group_id')
def _compute_picking_ids(self):
for order in self:
order.picking_ids = self.env['stock.picking'].search([
('group_id', '=', order.procurement_group_id.id),
])
order.delivery_count = len(order.picking_ids)
def action_view_mo_delivery(self):
""" This function returns an action that display picking related to
manufacturing order orders. It can either be a in a list or in a form
view, if there is only one picking to show.
"""
self.ensure_one()
action = self.env.ref('stock.action_picking_tree_all').read()[0]
pickings = self.mapped('picking_ids')
if len(pickings) > 1:
action['domain'] = [('id', 'in', pickings.ids)]
elif pickings:
action['views'] = [(self.env.ref('stock.view_picking_form').id, 'form')]
action['res_id'] = pickings.id
return action
@api.depends('product_uom_id', 'product_qty', 'product_id.uom_id')
def _compute_product_uom_qty(self):
@@ -433,12 +458,12 @@ class MrpProduction(models.Model):
@api.multi
def _adjust_procure_method(self):
try:
mto_route = self.env['stock.warehouse']._get_mto_route()
mto_route = self.env['stock.warehouse']._find_global_route('stock.route_warehouse0_mto', 'Make To Order')
except:
mto_route = False
for move in self.move_raw_ids:
product = move.product_id
routes = product.route_ids + product.route_from_categ_ids
routes = product.route_ids + product.route_from_categ_ids + move.warehouse_id.route_ids
# TODO: optimize with read_group?
pull = self.env['stock.rule'].search([('route_id', 'in', [x.id for x in routes]), ('location_src_id', '=', move.location_id.id),
('location_id', '=', move.location_dest_id.id), ('action', '!=', 'push')], limit=1)
@@ -576,7 +601,8 @@ class MrpProduction(models.Model):
finish_moves = production.move_finished_ids.filtered(lambda x: x.state not in ('done', 'cancel'))
raw_moves = production.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel'))
(finish_moves | raw_moves)._action_cancel()
picking_ids = production.picking_ids.filtered(lambda x: x.state not in ('done', 'cancel'))
picking_ids.action_cancel()
self.write({'state': 'cancel', 'is_locked': True})
return True
+1 -1
View File
@@ -66,7 +66,7 @@ class StockRule(models.Model):
'product_id': product_id.id,
'product_qty': product_qty,
'product_uom_id': product_uom.id,
'location_src_id': self.location_src_id.id or location_id.id,
'location_src_id': self.location_src_id.id or self.picking_type_id.default_location_src_id.id or location_id.id,
'location_dest_id': location_id.id,
'bom_id': bom.id,
'date_planned_start': fields.Datetime.to_string(self._get_date_planned(product_id, values)),
+217 -81
View File
@@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, exceptions, fields, models, _
from odoo import api, fields, models, _
from odoo.exceptions import UserError
class StockWarehouse(models.Model):
@@ -12,103 +13,225 @@ class StockWarehouse(models.Model):
help="When products are manufactured, they can be manufactured in this warehouse.")
manufacture_pull_id = fields.Many2one(
'stock.rule', 'Manufacture Rule')
pbm_mto_pull_id = fields.Many2one(
'stock.rule', 'Picking Before Manufacturing MTO Rule')
sam_rule_id = fields.Many2one(
'stock.rule', 'Stock After Manufacturing Rule')
manu_type_id = fields.Many2one(
'stock.picking.type', 'Manufacturing Operation Type',
domain=[('code', '=', 'mrp_operation')])
def create_sequences_and_picking_types(self):
res = super(StockWarehouse, self).create_sequences_and_picking_types()
self._create_manufacturing_picking_type()
return res
pbm_type_id = fields.Many2one('stock.picking.type', 'Picking Before Manufacturing Operation Type')
sam_type_id = fields.Many2one('stock.picking.type', 'Stock After Manufacturing Operation Type')
@api.multi
def get_routes_dict(self):
result = super(StockWarehouse, self).get_routes_dict()
manufacture_steps = fields.Selection([
('mrp_one_step', 'Manufacture (1 step)'),
('pbm', 'Pick components and then manufacture (2 steps)'),
('pbm_sam', 'Pick components, manufacture and then store products (3 steps)')],
'Manufacture', default='mrp_one_step', required=True,
help="Produce : Move the raw materials to the production location\
directly and start the manufacturing process.\nPick / Produce : Unload\
the raw materials from the Stock to Input location first, and then\
transfer it to the Production location.")
pbm_route_id = fields.Many2one('stock.location.route', 'Picking Before Manufacturing Route', ondelete='restrict')
pbm_loc_id = fields.Many2one('stock.location', 'Picking before Manufacturing Location')
sam_loc_id = fields.Many2one('stock.location', 'Stock after Manufacturing Location')
def get_rules_dict(self):
result = super(StockWarehouse, self).get_rules_dict()
production_location_id = self._get_production_location()
for warehouse in self:
result[warehouse.id]['manufacture'] = [self.Routing(warehouse.lot_stock_id, warehouse.lot_stock_id, warehouse.int_type_id, 'manufacture')]
result[warehouse.id].update({
'mrp_one_step': [],
'pbm': [
self.Routing(warehouse.lot_stock_id, warehouse.pbm_loc_id, warehouse.pbm_type_id, 'pull'),
self.Routing(warehouse.pbm_loc_id, production_location_id, warehouse.manu_type_id, 'pull'),
],
'pbm_sam': [
self.Routing(warehouse.lot_stock_id, warehouse.pbm_loc_id, warehouse.pbm_type_id, 'pull'),
self.Routing(warehouse.pbm_loc_id, production_location_id, warehouse.manu_type_id, 'pull'),
],
})
return result
def _get_manufacture_route_id(self):
manufacture_route = self.env.ref('mrp.route_warehouse0_manufacture', raise_if_not_found=False)
if not manufacture_route:
manufacture_route = self.env['stock.location.route'].search([('name', 'like', _('Manufacture'))], limit=1)
if not manufacture_route:
raise exceptions.UserError(_('Can\'t find any generic Manufacture route.'))
return manufacture_route.id
@api.model
def _get_production_location(self):
location = self.env.ref('stock.location_production', raise_if_not_found=False)
if not location:
location = self.env['stock.location'].search([('usage', '=', 'production')], limit=1)
if not location:
raise UserError(_('Can\'t find any production location.'))
return location
def _get_manufacture_pull_rules_values(self, route_values):
if not self.manu_type_id:
self._create_manufacturing_picking_type()
pull_rules_list = self._get_rule_values(route_values, values={
'name': self._format_routename(_(' Manufacture')),
'location_src_id': False, # TDE FIXME
'action': 'manufacture',
'route_id': self._get_manufacture_route_id(),
'picking_type_id': self.manu_type_id.id,
'propagate': False,
'active': True})
return pull_rules_list
def _get_routes_values(self):
routes = super(StockWarehouse, self)._get_routes_values()
routes.update({
'pbm_route_id': {
'routing_key': self.manufacture_steps,
'depends': ['manufacture_steps', 'manufacture_to_resupply'],
'route_update_values': {
'name': self._format_routename(route_type=self.manufacture_steps),
'active': self.manufacture_steps != 'mrp_one_step',
},
'route_create_values': {
'product_categ_selectable': True,
'warehouse_selectable': True,
'product_selectable': False,
'company_id': self.company_id.id,
'sequence': 10,
},
'rules_values': {
'active': True,
}
}
})
return routes
def _create_manufacturing_picking_type(self):
# TDE CLEANME
picking_type_obj = self.env['stock.picking.type']
seq_obj = self.env['ir.sequence']
for warehouse in self:
#man_seq_id = seq_obj.sudo().create('name': warehouse.name + _(' Sequence Manufacturing'), 'prefix': warehouse.code + '/MANU/', 'padding')
wh_stock_loc = warehouse.lot_stock_id
seq = seq_obj.search([('code', '=', 'mrp.production')], limit=1)
other_pick_type = picking_type_obj.search([('warehouse_id', '=', warehouse.id)], order = 'sequence desc', limit=1)
color = other_pick_type.color if other_pick_type else 0
max_sequence = other_pick_type and other_pick_type.sequence or 0
manu_type = picking_type_obj.create({
def _get_route_name(self, route_type):
names = {
'mrp_one_step': _('Manufacture (1 step)'),
'pbm': _('Pick components and then manufacture'),
'pbm_sam': _('Pick components, manufacture and then store products (3 steps)'),
}
if route_type in names:
return names[route_type]
else:
return super(StockWarehouse, self)._get_route_name(route_type)
def _get_global_route_rules_values(self):
rules = super(StockWarehouse, self)._get_global_route_rules_values()
location_id = self.manufacture_steps == 'pbm_sam' and self.sam_loc_id or self.lot_stock_id
rules.update({
'manufacture_pull_id': {
'depends': ['manufacture_steps', 'manufacture_to_resupply'],
'create_values': {
'action': 'manufacture',
'procure_method': 'make_to_order',
'picking_type_id': self.manu_type_id.id,
'route_id': self._find_global_route('mrp.route_warehouse0_manufacture', 'Manufacture').id
},
'update_values': {
'active': self.manufacture_to_resupply,
'name': self._format_rulename(location_id, False, 'Production'),
'location_id': location_id.id,
}
},
'pbm_mto_pull_id': {
'depends': ['manufacture_steps', 'manufacture_to_resupply'],
'create_values': {
'procure_method': 'make_to_order',
'company_id': self.company_id.id,
'action': 'pull',
'auto': 'manual',
'propagate': True,
'route_id': self._find_global_route('stock.route_warehouse0_mto', 'Make To Order').id,
'name': self._format_rulename(self.lot_stock_id, self.pbm_loc_id, 'MTO'),
'location_id': self.pbm_loc_id.id,
'location_src_id': self.lot_stock_id.id,
'picking_type_id': self.pbm_type_id.id
},
'update_values': {
'active': self.manufacture_steps != 'mrp_one_step',
}
},
# The purpose to move sam rule in the manufacture route instead of
# pbm_route_id is to avoid conflict with receipt in multiple
# step. For example if the product is manufacture and receipt in two
# step it would conflict in WH/Stock since product could come from
# WH/post-prod or WH/input. We do not have this conflict with
# manufacture route since it is set on the product.
'sam_rule_id': {
'depends': ['manufacture_steps', 'manufacture_to_resupply'],
'create_values': {
'procure_method': 'make_to_order',
'company_id': self.company_id.id,
'action': 'pull_push',
'auto': 'manual',
'propagate': True,
'route_id': self._find_global_route('mrp.route_warehouse0_manufacture', 'Manufacture').id,
'name': self._format_rulename(self.sam_loc_id, self.lot_stock_id, False),
'location_id': self.lot_stock_id.id,
'location_src_id': self.sam_loc_id.id,
'picking_type_id': self.sam_type_id.id
},
'update_values': {
'active': self.manufacture_steps == 'pbm_sam',
}
}
})
return rules
def _get_locations_values(self, vals):
values = super(StockWarehouse, self)._get_locations_values(vals)
def_values = self.default_get(['manufacture_steps'])
manufacture_steps = vals.get('manufacture_steps', def_values['manufacture_steps'])
values.update({
'pbm_loc_id': {'name': _('Pre-Production'), 'active': manufacture_steps in ('pbm', 'pbm_sam'), 'usage': 'internal'},
'sam_loc_id': {'name': _('Post-Production'), 'active': manufacture_steps == 'pbm_sam', 'usage': 'internal'},
})
return values
def _get_sequence_values(self):
values = super(StockWarehouse, self)._get_sequence_values()
values.update({
'pbm_type_id': {'name': self.name + ' ' + _('Sequence picking before manufacturing'), 'prefix': self.code + '/PC/', 'padding': 5},
'sam_type_id': {'name': self.name + ' ' + _('Sequence stock after manufacturing'), 'prefix': self.code + '/SFP/', 'padding': 5},
'manu_type_id': {'name': self.name + ' ' + _('Sequence production'), 'prefix': self.code + '/MO/', 'padding': 5},
})
return values
def _get_picking_type_create_values(self, max_sequence):
data, next_sequence = super(StockWarehouse, self)._get_picking_type_create_values(max_sequence)
data.update({
'pbm_type_id': {
'name': _('Pick Components'),
'code': 'internal',
'use_create_lots': True,
'use_existing_lots': True,
'default_location_src_id': self.lot_stock_id.id,
'default_location_dest_id': self.pbm_loc_id.id,
'sequence': next_sequence + 1
},
'sam_type_id': {
'name': _('Store Finished Product'),
'code': 'internal',
'use_create_lots': True,
'use_existing_lots': True,
'default_location_src_id': self.sam_loc_id.id,
'default_location_dest_id': self.lot_stock_id.id,
'sequence': next_sequence + 3
},
'manu_type_id': {
'name': _('Manufacturing'),
'warehouse_id': warehouse.id,
'code': 'mrp_operation',
'use_create_lots': True,
'use_existing_lots': False,
'sequence_id': seq.id,
'default_location_src_id': wh_stock_loc.id,
'default_location_dest_id': wh_stock_loc.id,
'sequence': max_sequence,
'color': color})
warehouse.write({'manu_type_id': manu_type.id})
'use_existing_lots': True,
'sequence': next_sequence + 2
},
})
return data, max_sequence + 4
def _create_or_update_manufacture_pull(self, routes_data):
routes_data = routes_data or self.get_routes_dict()
for warehouse in self:
routings = routes_data[warehouse.id]['manufacture']
if warehouse.manufacture_pull_id:
manufacture_pull = warehouse.manufacture_pull_id
manufacture_pull.write(warehouse._get_manufacture_pull_rules_values(routings)[0])
else:
manufacture_pull = self.env['stock.rule'].create(warehouse._get_manufacture_pull_rules_values(routings)[0])
return manufacture_pull
@api.multi
def create_routes(self):
res = super(StockWarehouse, self).create_routes()
self.ensure_one()
routes_data = self.get_routes_dict()
manufacture_pull = self._create_or_update_manufacture_pull(routes_data)
res['manufacture_pull_id'] = manufacture_pull.id
return res
def _get_picking_type_update_values(self):
data = super(StockWarehouse, self)._get_picking_type_update_values()
data.update({
'pbm_type_id': {'active': self.manufacture_to_resupply and self.manufacture_steps in ('pbm', 'pbm_sam')},
'sam_type_id': {'active': self.manufacture_to_resupply and self.manufacture_steps == 'pbm_sam'},
'manu_type_id': {
'active': self.manufacture_to_resupply,
'default_location_src_id': self.manufacture_steps in ('pbm', 'pbm_sam') and self.pbm_loc_id.id or self.lot_stock_id.id,
'default_location_dest_id': self.manufacture_steps == 'pbm_sam' and self.sam_loc_id or self.lot_stock_id.id,
},
})
return data
@api.multi
def write(self, vals):
if 'manufacture_to_resupply' in vals:
if vals.get("manufacture_to_resupply"):
for warehouse in self.filtered(lambda warehouse: not warehouse.manufacture_pull_id):
manufacture_pull = warehouse._create_or_update_manufacture_pull(self.get_routes_dict())
vals['manufacture_pull_id'] = manufacture_pull.id
for warehouse in self:
if not warehouse.manu_type_id:
warehouse._create_manufacturing_picking_type()
warehouse.manu_type_id.active = True
else:
for warehouse in self:
if warehouse.manu_type_id:
warehouse.manu_type_id.active = False
self.mapped('manufacture_pull_id').unlink()
if any(field in vals for field in ('manufacture_steps', 'manufacture_to_resupply')):
for warehouse in self:
warehouse._update_location_manufacture(vals.get('manufacture_steps', warehouse.manufacture_steps))
return super(StockWarehouse, self).write(vals)
@api.multi
@@ -117,6 +240,19 @@ class StockWarehouse(models.Model):
routes |= self.filtered(lambda self: self.manufacture_to_resupply and self.manufacture_pull_id and self.manufacture_pull_id.route_id).mapped('manufacture_pull_id').mapped('route_id')
return routes
def _update_location_manufacture(self, new_manufacture_step):
switch_warehouses = self.filtered(lambda wh: wh.manufacture_steps != new_manufacture_step)
loc_warehouse = switch_warehouses.filtered(lambda wh: not wh._location_used(wh.pbm_loc_id))
if loc_warehouse:
loc_warehouse.mapped('pbm_loc_id').write({'active': False})
loc_warehouse = switch_warehouses.filtered(lambda wh: not wh._location_used(wh.sam_loc_id))
if loc_warehouse:
loc_warehouse.mapped('sam_loc_id').write({'active': False})
if new_manufacture_step != 'mrp_one_step':
self.mapped('pbm_loc_id').write({'active': True})
if new_manufacture_step == 'pbm_sam':
self.mapped('sam_loc_id').write({'active': True})
@api.multi
def _update_name_and_code(self, name=False, code=False):
res = super(StockWarehouse, self)._update_name_and_code(name, code)
+1
View File
@@ -3,6 +3,7 @@
from . import test_bom
from . import test_order
from . import test_stock
from . import test_warehouse_multistep_manufacturing
from . import test_workorder_operation
from . import test_procurement
from . import test_unbuild
+4 -1
View File
@@ -66,7 +66,10 @@ class TestBoM(TestMrpCommon):
tmp_picking_type = self.env['stock.picking.type'].create({
'name': 'Manufacturing',
'code': 'mrp_operation',
'sequence_id': self.env['ir.sequence'].search([('code', '=', 'mrp.production')], limit=1).id,
'sequence_id': self.env['ir.sequence'].create({
'code': 'mrp.production',
'name': 'tmp_production_sequence',
}).id,
})
test_bom_1 = self.env['mrp.bom'].create({
'product_tmpl_id': self.product_5.product_tmpl_id.id,
+1 -1
View File
@@ -18,7 +18,7 @@ class TestWarehouse(common.TestMrpCommon):
warehouse_1_stock_manager.write({
'manufacture_to_resupply': False
})
self.assertFalse(self.warehouse_1.manufacture_pull_id)
self.assertFalse(self.warehouse_1.manufacture_pull_id.active)
self.assertFalse(self.warehouse_1.manu_type_id.active)
self.assertNotIn(manu_route, warehouse_1_stock_manager._get_all_routes())
warehouse_1_stock_manager.write({
@@ -0,0 +1,169 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import Form
from odoo.addons.mrp.tests.common import TestMrpCommon
class TestMultistepManufacturingWarehouse(TestMrpCommon):
def setUp(self):
super(TestMultistepManufacturingWarehouse, self).setUp()
# Create warehouse
self.customer_location = self.env['ir.model.data'].xmlid_to_res_id('stock.stock_location_customers')
warehouse_form = Form(self.env['stock.warehouse'])
warehouse_form.name = 'Test Warehouse'
warehouse_form.code = 'TWH'
self.warehouse = warehouse_form.save()
self.uom_unit = self.env.ref('uom.product_uom_unit')
# Create manufactured product
product_form = Form(self.env['product.product'])
product_form.name = 'Stick'
product_form.uom_id = self.uom_unit
product_form.uom_po_id = self.uom_unit
product_form.type = 'product'
product_form.route_ids.clear()
product_form.route_ids.add(self.warehouse.manufacture_pull_id.route_id)
product_form.route_ids.add(self.warehouse.mto_pull_id.route_id)
self.finished_product = product_form.save()
# Create raw product for manufactured product
product_form = Form(self.env['product.product'])
product_form.name = 'Raw Stick'
product_form.type = 'product'
product_form.uom_id = self.uom_unit
product_form.uom_po_id = self.uom_unit
self.raw_product = product_form.save()
# Create bom for manufactured product
bom_product_form = Form(self.env['mrp.bom'])
bom_product_form.product_id = self.finished_product
bom_product_form.product_tmpl_id = self.finished_product.product_tmpl_id
bom_product_form.product_qty = 1.0
bom_product_form.type = 'normal'
with bom_product_form.bom_line_ids.new() as bom_line:
bom_line.product_id = self.raw_product
bom_line.product_qty = 2.0
self.bom = bom_product_form.save()
def _check_location_and_routes(self):
# Check manufacturing pull rule.
self.assertTrue(self.warehouse.manufacture_pull_id)
self.assertTrue(self.warehouse.manufacture_pull_id.active, self.warehouse.manufacture_to_resupply)
self.assertTrue(self.warehouse.manufacture_pull_id.route_id)
# Check new routes created or not.
self.assertTrue(self.warehouse.pbm_route_id)
# Check location should be created and linked to warehouse.
self.assertTrue(self.warehouse.pbm_loc_id)
self.assertEqual(self.warehouse.pbm_loc_id.active, self.warehouse.manufacture_steps != 'mrp_one_step', "Input location must be de-active for single step only.")
self.assertTrue(self.warehouse.manu_type_id.active)
def test_00_create_warehouse(self):
""" Warehouse testing for direct manufacturing """
with Form(self.warehouse) as warehouse:
warehouse.manufacture_steps = 'mrp_one_step'
self._check_location_and_routes()
# Check locations of existing pull rule
self.assertFalse(self.warehouse.pbm_route_id.rule_ids, 'only the update of global manufacture route should happen.')
self.assertEqual(self.warehouse.manufacture_pull_id.location_id.id, self.warehouse.lot_stock_id.id)
def test_01_warehouse_twostep_manufacturing(self):
""" Warehouse testing for picking before manufacturing """
with Form(self.warehouse) as warehouse:
warehouse.manufacture_steps = 'pbm'
self._check_location_and_routes()
self.assertEqual(len(self.warehouse.pbm_route_id.rule_ids), 2)
self.assertEqual(self.warehouse.manufacture_pull_id.location_id.id, self.warehouse.lot_stock_id.id)
def test_02_warehouse_twostep_manufacturing(self):
""" Warehouse testing for picking ans store after manufacturing """
with Form(self.warehouse) as warehouse:
warehouse.manufacture_steps = 'pbm_sam'
self._check_location_and_routes()
self.assertEqual(len(self.warehouse.pbm_route_id.rule_ids), 2)
self.assertEqual(self.warehouse.manufacture_pull_id.location_id.id, self.warehouse.sam_loc_id.id)
def test_manufacturing_flow(self):
""" Simulate a pick pack ship delivery combined with a picking before
manufacturing and store after manufacturing. Also ensure that the MO and
the moves to stock are created with the generic pull rules.
In order to trigger the rule we create a picking to the customer with
the 'make to order' procure method
"""
with Form(self.warehouse) as warehouse:
warehouse.manufacture_steps = 'pbm_sam'
warehouse.delivery_steps = 'pick_pack_ship'
self.env['stock.quant']._update_available_quantity(self.raw_product, self.warehouse.lot_stock_id, 4.0)
picking_customer = self.env['stock.picking'].create({
'location_id': self.warehouse.wh_output_stock_loc_id.id,
'location_dest_id': self.customer_location,
'partner_id': self.env['ir.model.data'].xmlid_to_res_id('base.res_partner_4'),
'picking_type_id': self.warehouse.out_type_id.id,
})
self.env['stock.move'].create({
'name': self.finished_product.name,
'product_id': self.finished_product.id,
'product_uom_qty': 2,
'product_uom': self.uom_unit.id,
'picking_id': picking_customer.id,
'location_id': self.warehouse.wh_output_stock_loc_id.id,
'location_dest_id': self.customer_location,
'procure_method': 'make_to_order',
'state': 'draft',
})
picking_customer.action_confirm()
production_order = self.env['mrp.production'].search([('product_id', '=', self.finished_product.id)])
self.assertTrue(production_order)
picking_stock_preprod = self.env['stock.move'].search([
('product_id', '=', self.raw_product.id),
('location_id', '=', self.warehouse.lot_stock_id.id),
('location_dest_id', '=', self.warehouse.pbm_loc_id.id),
('picking_type_id', '=', self.warehouse.pbm_type_id.id)
]).picking_id
picking_stock_postprod = self.env['stock.move'].search([
('product_id', '=', self.finished_product.id),
('location_id', '=', self.warehouse.sam_loc_id.id),
('location_dest_id', '=', self.warehouse.lot_stock_id.id),
('picking_type_id', '=', self.warehouse.sam_type_id.id)
]).picking_id
self.assertTrue(picking_stock_preprod)
self.assertTrue(picking_stock_postprod)
self.assertEqual(picking_stock_preprod.state, 'confirmed')
self.assertEqual(picking_stock_postprod.state, 'waiting')
picking_stock_preprod.action_assign()
picking_stock_preprod.move_line_ids.qty_done = 4
picking_stock_preprod.action_done()
self.assertFalse(self.env['stock.quant']._gather(self.raw_product, self.warehouse.lot_stock_id))
self.assertTrue(self.env['stock.quant']._gather(self.raw_product, self.warehouse.pbm_loc_id))
production_order.action_assign()
self.assertEqual(production_order.availability, 'assigned')
self.assertEqual(picking_stock_postprod.state, 'waiting')
produce_form = Form(self.env['mrp.product.produce'].with_context({
'active_id': production_order.id,
'active_ids': [production_order.id],
}))
produce_form.product_qty = production_order.product_qty
product_produce = produce_form.save()
product_produce.do_produce()
production_order.button_mark_done()
self.assertFalse(self.env['stock.quant']._gather(self.raw_product, self.warehouse.pbm_loc_id))
self.assertEqual(picking_stock_postprod.state, 'assigned')
picking_stock_pick = self.env['stock.move'].search([
('product_id', '=', self.finished_product.id),
('location_id', '=', self.warehouse.lot_stock_id.id),
('location_dest_id', '=', self.warehouse.wh_pack_stock_loc_id.id),
('picking_type_id', '=', self.warehouse.pick_type_id.id)
]).picking_id
self.assertEqual(picking_stock_pick.move_lines.move_orig_ids.picking_id, picking_stock_postprod)
@@ -64,6 +64,9 @@
</div>
</button>
<button name="%(action_mrp_production_moves)d" type="action" string="Inventory Moves" class="oe_stat_button" icon="fa-arrows-v" attrs="{'invisible': [('state', 'not in', ('progress', 'done'))]}"/>
<button type="object" name="action_view_mo_delivery" class="oe_stat_button" icon="fa-truck" attrs="{'invisible': [('delivery_count', '=', 0)]}" groups="base.group_user">
<field name="delivery_count" widget="statinfo" string="Picking"/>
</button>
<button class="oe_stat_button" name="action_see_move_scrap" type="object" icon="fa-arrows-v" attrs="{'invisible': [('scrap_count', '=', 0)]}">
<div class="o_field_widget o_stat_info">
<span class="o_stat_value"><field name="scrap_count"/></span>
+16 -4
View File
@@ -1,17 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Warehouse -->
<record id="view_warehouse_inherited" model="ir.ui.view">
<field name="name">Stock Warehouse Inherited</field>
<record id="view_warehouse_inherit_mrp" model="ir.ui.view">
<field name="name">Stock Warehouse Inherit MRP</field>
<field name="model">stock.warehouse</field>
<field name="inherit_id" ref="stock.view_warehouse"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='resupply_wh_ids']" position="before">
<xpath expr="//field[@name='resupply_wh_ids']" position="before">
<field name="manufacture_to_resupply" />
<field name="manufacture_steps" attrs="{'invisible': [('manufacture_to_resupply', '=', False)]}" widget="radio"/>
</xpath>
<xpath expr="//field[@name='out_type_id']" position="after">
<field name="manu_type_id" readonly="True"/>
</xpath>
<xpath expr="//group[@name='group_resupply']" position="attributes">
<attribute name="attrs">{}</attribute>
</xpath>
<xpath expr="//field[@name='wh_output_stock_loc_id']" position="after">
<field name="sam_loc_id"/>
<field name="pbm_loc_id"/>
</xpath>
<xpath expr="//field[@name='out_type_id']" position="after">
<field name="sam_type_id"/>
<field name="pbm_type_id"/>
</xpath>
</field>
</record>
</odoo>
</odoo>
@@ -30,6 +30,8 @@ class MrpProduction(models.Model):
'warehouse_id': production.location_dest_id.get_warehouse().id,
'origin': production.name,
'unit_factor': qty1 / (production.product_qty - production.qty_produced),
'propagate': self.propagate,
'group_id': self.move_dest_ids and self.move_dest_ids.mapped('group_id')[0].id or self.procurement_group_id.id,
'subproduct_id': sub_product.id
}
move = Move.create(data)
+1 -1
View File
@@ -15,4 +15,4 @@ def _create_buy_rules(cr, registry):
env = api.Environment(cr, SUPERUSER_ID, {})
warehouse_ids = env['stock.warehouse'].search([('buy_pull_id', '=', False)])
for warehouse_id in warehouse_ids:
warehouse_id.buy_pull_id = warehouse_id._create_buy_rule()
warehouse_id._create_or_update_global_routes_rules()
+19 -49
View File
@@ -115,43 +115,25 @@ class StockWarehouse(models.Model):
help="When products are bought, they can be delivered to this warehouse")
buy_pull_id = fields.Many2one('stock.rule', 'Buy rule')
@api.multi
def _get_buy_pull_rule(self):
buy_route_id = self.env.ref('purchase_stock.route_warehouse0_buy', raise_if_not_found=False)
if not buy_route_id:
buy_route_id = self.env['stock.location.route'].search([('name', 'like', _('Buy'))], limit=1)
if not buy_route_id:
raise UserError(_("Can't find any Buy route. Please create a route with the 'Buy' action for your receipts operation types."))
return {
'name': self._format_routename(_(' Buy')),
'location_id': self.in_type_id.default_location_dest_id.id,
'route_id': buy_route_id.id,
'action': 'buy',
'picking_type_id': self.in_type_id.id,
'warehouse_id': self.id,
'group_propagation_option': 'none',
}
@api.multi
def create_routes(self):
res = super(StockWarehouse, self).create_routes() # super applies ensure_one()
if self.buy_to_resupply:
res['buy_pull_id'] = self._create_buy_rule()
return res
@api.multi
def write(self, vals):
if 'buy_to_resupply' in vals:
if vals.get("buy_to_resupply"):
for warehouse in self:
if not warehouse.buy_pull_id:
vals['buy_pull_id'] = self._create_buy_rule()
else:
for warehouse in self:
if warehouse.buy_pull_id:
warehouse.buy_pull_id.unlink()
return super(StockWarehouse, self).write(vals)
def _get_global_route_rules_values(self):
rules = super(StockWarehouse, self)._get_global_route_rules_values()
location_id = self.in_type_id.default_location_dest_id
rules.update({
'buy_pull_id': {
'depends': ['reception_steps', 'buy_to_resupply'],
'create_values': {
'action': 'buy',
'picking_type_id': self.in_type_id.id,
'route_id': self._find_global_route('purchase.route_warehouse0_buy', 'Buy').id
},
'update_values': {
'active': self.buy_to_resupply,
'name': self._format_rulename(location_id, False, 'Buy'),
'location_id': location_id.id,
}
}
})
return rules
@api.multi
def _get_all_routes(self):
@@ -168,18 +150,6 @@ class StockWarehouse(models.Model):
warehouse.buy_pull_id.write({'name': warehouse.buy_pull_id.name.replace(warehouse.name, name, 1)})
return res
@api.multi
def _update_routes(self):
res = super(StockWarehouse, self)._update_routes()
for warehouse in self:
if warehouse.in_type_id.default_location_dest_id != warehouse.buy_pull_id.location_id:
warehouse.buy_pull_id.write({'location_id': warehouse.in_type_id.default_location_dest_id.id})
return res
def _create_buy_rule(self):
buy_pull_vals = self._get_buy_pull_rule()
buy_pull = self.env['stock.rule'].create(buy_pull_vals)
return buy_pull.id
class ReturnPicking(models.TransientModel):
_inherit = "stock.return.picking"
@@ -19,6 +19,9 @@
<xpath expr="//field[@name='resupply_wh_ids']" position="before">
<field name="buy_to_resupply" />
</xpath>
<xpath expr="//group[@name='group_resupply']" position="attributes">
<attribute name="attrs">{}</attribute>
</xpath>
</field>
</record>
+1
View File
@@ -4,3 +4,4 @@
from . import test_sale_mrp_flow
from . import test_sale_mrp_lead_time
from . import test_sale_mrp_procurement
from . import test_multistep_manufacturing
@@ -0,0 +1,87 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import Form
from odoo.addons.mrp.tests.common import TestMrpCommon
class TestMultistepManufacturing(TestMrpCommon):
def setUp(self):
super(TestMultistepManufacturing, self).setUp()
self.MrpProduction = self.env['mrp.production']
# Create warehouse
warehouse_form = Form(self.env['stock.warehouse'])
warehouse_form.name = 'Test'
warehouse_form.code = 'Test'
self.warehouse = warehouse_form.save()
self.uom_unit = self.env.ref('uom.product_uom_unit')
# Create manufactured product
product_form = Form(self.env['product.product'])
product_form.name = 'Stick'
product_form.uom_id = self.uom_unit
product_form.uom_po_id = self.uom_unit
product_form.route_ids.clear()
product_form.route_ids.add(self.warehouse.manufacture_pull_id.route_id)
product_form.route_ids.add(self.warehouse.mto_pull_id.route_id)
self.product_manu = product_form.save()
# Create raw product for manufactured product
product_form = Form(self.env['product.product'])
product_form.name = 'Raw Stick'
product_form.uom_id = self.uom_unit
product_form.uom_po_id = self.uom_unit
self.product_raw = product_form.save()
# Create bom for manufactured product
bom_product_form = Form(self.env['mrp.bom'])
bom_product_form.product_id = self.product_manu
bom_product_form.product_tmpl_id = self.product_manu.product_tmpl_id
bom_product_form.product_qty = 1.0
bom_product_form.type = 'normal'
with bom_product_form.bom_line_ids.new() as bom_line:
bom_line.product_id = self.product_raw
bom_line.product_qty = 2.0
self.bom_prod_manu = bom_product_form.save()
# Create sale order
sale_form = Form(self.env['sale.order'])
sale_form.partner_id = self.env.ref('base.res_partner_1')
sale_form.picking_policy = 'direct'
sale_form.warehouse_id = self.warehouse
with sale_form.order_line.new() as line:
line.name = self.product_manu.name
line.product_id = self.product_manu
line.product_uom_qty = 1.0
line.product_uom = self.uom_unit
line.price_unit = 10.0
self.sale_order = sale_form.save()
def test_00_manufacturing_step_one(self):
""" Testing for Step-1 """
# Change steps of manufacturing.
with Form(self.warehouse) as warehouse:
warehouse.manufacture_steps = 'mrp_one_step'
# Confirm sale order.
self.sale_order.action_confirm()
# Check all procurements for created sale order
mo_procurement = self.MrpProduction.search([('origin', '=', self.sale_order.name)])
# Get manufactured procurement
self.assertEqual(mo_procurement.location_src_id.id, self.warehouse.lot_stock_id.id, "Source loction does not match.")
self.assertEqual(mo_procurement.location_dest_id.id, self.warehouse.lot_stock_id.id, "Destination location does not match.")
self.assertEqual(len(mo_procurement), 1, "No Procurement !")
def test_01_manufacturing_step_two(self):
""" Testing for Step-2 """
with Form(self.warehouse) as warehouse:
warehouse.manufacture_steps = 'pbm'
self.sale_order.action_confirm()
# Get manufactured procurement
mo_procurement = self.MrpProduction.search([('origin', '=', self.sale_order.name)])
self.assertEqual(mo_procurement.location_src_id.id, self.warehouse.pbm_loc_id.id, "Source loction does not match.")
self.assertEqual(mo_procurement.location_dest_id.id, self.warehouse.lot_stock_id.id, "Destination location does not match.")
self.assertEqual(len(mo_procurement), 1, "No Procurement !")
+2 -2
View File
@@ -310,7 +310,7 @@ class SaleOrderLine(models.Model):
'date_planned': date_planned,
'route_ids': self.route_id,
'warehouse_id': self.order_id.warehouse_id or False,
'partner_dest_id': self.order_id.partner_shipping_id
'partner_id': self.order_id.partner_shipping_id.id,
})
for line in self.filtered("order_id.commitment_date"):
date_planned = fields.Datetime.from_string(line.order_id.commitment_date) - timedelta(days=line.order_id.company_id.security_lead)
@@ -406,7 +406,7 @@ class SaleOrderLine(models.Model):
else:
mto_route = False
try:
mto_route = self.env['stock.warehouse']._get_mto_route()
mto_route = self.env['stock.warehouse']._find_global_route('stock.route_warehouse0_mto', 'Make To Order')
except UserError:
# if route MTO not found in ir_model_data, we treat the product as in MTS
pass
+4 -7
View File
@@ -45,13 +45,10 @@ class ProcurementGroup(models.Model):
class StockRule(models.Model):
_inherit = 'stock.rule'
def _get_stock_move_values(self, product_id, product_qty, product_uom, location_id, name, origin, values, group_id):
result = super(StockRule, self)._get_stock_move_values(product_id, product_qty, product_uom, location_id, name, origin, values, group_id)
if values.get('sale_line_id', False):
result['sale_line_id'] = values['sale_line_id']
if values.get('partner_dest_id'):
result['partner_id'] = values['partner_dest_id'].id
return result
def _get_custom_move_fields(self):
fields = super(StockRule, self)._get_custom_move_fields()
fields += ['sale_line_id', 'partner_id']
return fields
class StockPicking(models.Model):
+11 -1
View File
@@ -186,6 +186,12 @@ class StockRule(models.Model):
move._action_confirm()
return True
def _get_custom_move_fields(self):
""" The purpose of this method is to be override in order to easily add
fields from procurement 'values' argument to move data.
"""
return []
def _get_stock_move_values(self, product_id, product_qty, product_uom, location_id, name, origin, values, group_id):
''' Returns a dictionary of values that will be used to create a stock move from a procurement.
This function assumes that the given procurement has a rule (action == 'pull' or 'pull_push') set on it.
@@ -199,7 +205,7 @@ class StockRule(models.Model):
# it is possible that we've already got some move done, so check for the done qty and create
# a new move with the correct qty
qty_left = product_qty
return {
move_values = {
'name': name[:2000],
'company_id': self.company_id.id or self.location_src_id.company_id.id or self.location_id.company_id.id or values['company_id'].id,
'product_id': product_id.id,
@@ -221,6 +227,10 @@ class StockRule(models.Model):
'propagate': self.propagate,
'priority': values.get('priority', "1"),
}
for field in self._get_custom_move_fields():
if field in values:
move_values[field] = values.get(field)
return move_values
def _log_next_activity(self, product_id, note):
existing_activity = self.env['mail.activity'].search([('res_id', '=', product_id.product_tmpl_id.id), ('res_model_id', '=', self.env.ref('product.model_product_template').id),
+368 -246
View File
@@ -68,6 +68,7 @@ class Warehouse(models.Model):
'stock.location.route', 'supplied_wh_id', 'Resupply Routes',
help="Routes will be created for these resupply warehouses and you can select them on products and product categories")
warehouse_count = fields.Integer(compute='_compute_warehouse_count')
show_resupply = fields.Boolean(compute="_compute_show_resupply")
_sql_constraints = [
('warehouse_name_uniq', 'unique(name, company_id)', 'The name of the warehouse must be unique per company!'),
('warehouse_code_uniq', 'unique(code, company_id)', 'The code of the warehouse must be unique per company!'),
@@ -78,6 +79,10 @@ class Warehouse(models.Model):
for warehouse in self:
warehouse.warehouse_count = self.env['stock.warehouse'].search_count([('id', 'not in', warehouse.ids)])
def _compute_show_resupply(self):
for warehouse in self:
warehouse.show_resupply = warehouse.user_has_groups("stock.group_stock_multi_warehouses") and warehouse.warehouse_count
@api.model
def create(self, vals):
# create view location for warehouse then create all locations
@@ -86,17 +91,8 @@ class Warehouse(models.Model):
if vals.get('company_id'):
loc_vals['company_id'] = vals.get('company_id')
vals['view_location_id'] = self.env['stock.location'].create(loc_vals).id
sub_locations = self._get_locations_values(vals)
def_values = self.default_get(['reception_steps', 'delivery_steps'])
reception_steps = vals.get('reception_steps', def_values['reception_steps'])
delivery_steps = vals.get('delivery_steps', def_values['delivery_steps'])
sub_locations = {
'lot_stock_id': {'name': _('Stock'), 'active': True, 'usage': 'internal'},
'wh_input_stock_loc_id': {'name': _('Input'), 'active': reception_steps != 'one_step', 'usage': 'internal'},
'wh_qc_stock_loc_id': {'name': _('Quality Control'), 'active': reception_steps == 'three_steps', 'usage': 'internal'},
'wh_output_stock_loc_id': {'name': _('Output'), 'active': delivery_steps != 'ship_only', 'usage': 'internal'},
'wh_pack_stock_loc_id': {'name': _('Packing Zone'), 'active': delivery_steps == 'pick_pack_ship', 'usage': 'internal'},
}
for field_name, values in sub_locations.items():
values['location_id'] = vals['view_location_id']
if vals.get('company_id'):
@@ -106,11 +102,18 @@ class Warehouse(models.Model):
# actually create WH
warehouse = super(Warehouse, self).create(vals)
# create sequences and operation types
new_vals = warehouse.create_sequences_and_picking_types()
new_vals = warehouse._create_or_update_sequences_and_picking_types()
warehouse.write(new_vals) # TDE FIXME: use super ?
# create routes and push/stock rules
route_vals = warehouse.create_routes()
route_vals = warehouse._create_or_update_route()
warehouse.write(route_vals)
# Update global route with specific warehouse rule.
warehouse._create_or_update_global_routes_rules()
# create route selectable on the product to resupply the warehouse from another one
warehouse.create_resupply_routes(warehouse.resupply_wh_ids)
# update partner data if partner assigned
if vals.get('partner_id'):
self._update_partner_data(vals['partner_id'], vals.get('company_id'))
@@ -123,7 +126,8 @@ class Warehouse(models.Model):
if vals.get('code') or vals.get('name'):
warehouses._update_name_and_code(vals.get('name'), vals.get('code'))
# activate and deactivate location according to reception and delivery option
warehouses._create_missing_locations(vals)
if vals.get('reception_steps'):
warehouses._update_location_reception(vals['reception_steps'])
if vals.get('delivery_steps'):
@@ -142,11 +146,25 @@ class Warehouse(models.Model):
res = super(Warehouse, self).write(vals)
# check if we need to delete and recreate route
if vals.get('reception_steps') or vals.get('delivery_steps'):
route_vals = warehouses._update_routes()
if route_vals:
self.write(route_vals)
for warehouse in self:
# check if we need to delete and recreate route
depends = [depend for depends in [value['depends'] for value in warehouse._get_routes_values().values()] for depend in depends]
if any(depend in vals for depend in depends):
picking_type_vals = warehouse._create_or_update_sequences_and_picking_types()
if picking_type_vals:
warehouse.write(picking_type_vals)
route_vals = warehouse._create_or_update_route()
if route_vals:
warehouse.write(route_vals)
# Check if a global rule(mto, buy, ...) need to be modify.
# The field that impact those rules are listed in the
# _get_global_route_rules_values method under the key named
# 'depends'.
global_rules = warehouse._get_global_route_rules_values()
depends = [depend for depends in [value['depends'] for value in global_rules.values()] for depend in depends]
if any(rule in vals for rule in global_rules) or\
any(depend in vals for depend in depends):
warehouse._create_or_update_global_routes_rules()
if vals.get('resupply_wh_ids') and not vals.get('resupply_route_ids'):
for warehouse in warehouses:
@@ -172,100 +190,237 @@ class Warehouse(models.Model):
transit_loc = ResCompany._company_default_get('stock.warehouse').internal_transit_location_id.id
self.env['res.partner'].browse(partner_id).write({'property_stock_customer': transit_loc, 'property_stock_supplier': transit_loc})
def create_sequences_and_picking_types(self):
def _create_or_update_sequences_and_picking_types(self):
""" Create or update existing picking types for a warehouse.
Pikcing types are stored on the warehouse in a many2one. If the picking
type exist this method will update it. The update values can be found in
the method _get_picking_type_update_values. If the picking type does not
exist it will be created with a new sequence associated to it.
"""
self.ensure_one()
IrSequenceSudo = self.env['ir.sequence'].sudo()
PickingType = self.env['stock.picking.type']
input_loc, output_loc = self._get_input_output_locations(self.reception_steps, self.delivery_steps)
# choose the next available color for the operation types of this warehouse
all_used_colors = [res['color'] for res in PickingType.search_read([('warehouse_id', '!=', False), ('color', '!=', False)], ['color'], order='color')]
available_colors = [zef for zef in range(0, 12) if zef not in all_used_colors]
color = available_colors[0] if available_colors else 0
# suit for each warehouse: reception, internal, pick, pack, ship
max_sequence = PickingType.search_read([('sequence', '!=', False)], ['sequence'], limit=1, order='sequence desc')
max_sequence = max_sequence and max_sequence[0]['sequence'] or 0
warehouse_data = {}
sequence_data = self._get_sequence_values()
# tde todo: backport sequence fix
create_data = {
'in_type_id': {
'name': _('Receipts'),
'code': 'incoming',
'use_create_lots': True,
'use_existing_lots': False,
'default_location_src_id': False,
'sequence': max_sequence + 1,
}, 'out_type_id': {
'name': _('Delivery Orders'),
'code': 'outgoing',
'use_create_lots': False,
'use_existing_lots': True,
'default_location_dest_id': False,
'sequence': max_sequence + 5,
}, 'pack_type_id': {
'name': _('Pack'),
'code': 'internal',
'use_create_lots': False,
'use_existing_lots': True,
'default_location_src_id': self.wh_pack_stock_loc_id.id,
'default_location_dest_id': output_loc.id,
'sequence': max_sequence + 4,
}, 'pick_type_id': {
'name': _('Pick'),
'code': 'internal',
'use_create_lots': False,
'use_existing_lots': True,
'default_location_src_id': self.lot_stock_id.id,
'sequence': max_sequence + 3,
}, 'int_type_id': {
'name': _('Internal Transfers'),
'code': 'internal',
'use_create_lots': False,
'use_existing_lots': True,
'default_location_src_id': self.lot_stock_id.id,
'default_location_dest_id': self.lot_stock_id.id,
'active': self.reception_steps != 'one_step' or self.delivery_steps != 'ship_only' or self.user_has_groups('stock.group_stock_multi_locations'),
'sequence': max_sequence + 2,
},
}
data = self._get_picking_type_values(self.reception_steps, self.delivery_steps, self.wh_pack_stock_loc_id)
for field_name in data:
data[field_name].update(create_data[field_name])
# suit for each warehouse: reception, internal, pick, pack, ship
max_sequence = self.env['stock.picking.type'].search_read([('sequence', '!=', False)], ['sequence'], limit=1, order='sequence desc')
max_sequence = max_sequence and max_sequence[0]['sequence'] or 0
data = self._get_picking_type_update_values()
create_data, max_sequence = self._get_picking_type_create_values(max_sequence)
for picking_type, values in data.items():
sequence = IrSequenceSudo.create(sequence_data[picking_type])
values.update(warehouse_id=self.id, color=color, sequence_id=sequence.id)
warehouse_data[picking_type] = PickingType.create(values).id
PickingType.browse(warehouse_data['out_type_id']).write({'return_picking_type_id': warehouse_data['in_type_id']})
PickingType.browse(warehouse_data['in_type_id']).write({'return_picking_type_id': warehouse_data['out_type_id']})
if self[picking_type]:
self[picking_type].update(values)
else:
data[picking_type].update(create_data[picking_type])
sequence = IrSequenceSudo.create(sequence_data[picking_type])
values.update(warehouse_id=self.id, color=color, sequence_id=sequence.id)
warehouse_data[picking_type] = PickingType.create(values).id
if 'out_type_id' in warehouse_data:
PickingType.browse(warehouse_data['out_type_id']).write({'return_picking_type_id': warehouse_data['in_type_id']})
if 'in_type_id' in warehouse_data:
PickingType.browse(warehouse_data['in_type_id']).write({'return_picking_type_id': warehouse_data['out_type_id']})
return warehouse_data
def create_routes(self):
self.ensure_one()
routes_data = self.get_routes_dict()
def _create_or_update_global_routes_rules(self):
""" Some rules are not specific to a warehouse(e.g MTO, Buy, ...)
however they contain rule(s) for a specific warehouse. This method will
update the rules contained in global routes in order to make them match
with the wanted reception, delivery,... steps.
"""
for rule_field, rule_details in self._get_global_route_rules_values().items():
values = rule_details['update_values']
if self[rule_field]:
self[rule_field].write(values)
else:
values.update(rule_details['create_values'])
values.update({'warehouse_id': self.id})
self[rule_field] = self.env['stock.rule'].create(values)
return True
reception_route = self._create_or_update_reception_route(routes_data)
delivery_route = self._create_or_update_delivery_route(routes_data)
mto_pull = self._create_or_update_mto_pull(routes_data)
crossdock_route = self._create_or_update_crossdock_route(routes_data)
def _find_global_route(self, xml_id, route_name):
""" return a route record set from an xml_id or its name. """
route = self.env.ref(xml_id, raise_if_not_found=False)
if not route:
route = self.env['stock.location.route'].search([('name', 'like', _(route_name))], limit=1)
if not route:
raise UserError(_('Can\'t find any generic route %s.') % (route_name))
return route
# create route selectable on the product to resupply the warehouse from another one
self.create_resupply_routes(self.resupply_wh_ids)
# return routes and mto stock rule to store on the warehouse
def _get_global_route_rules_values(self):
""" Method used by _create_or_update_global_routes_rules. It's
purpose is to return a dict with this format.
key: The rule contained in a global route that have to be create/update
entry a dict with the following values:
-depends: Field that impact the rule. When a field in depends is
write on the warehouse the rule set as key have to be update.
-create_values: values used in order to create the rule if it does
not exist.
-update_values: values used to update the route when a field in
depends is modify on the warehouse.
"""
# We use 0 since routing are order from stock to cust. If the routing
# order is modify, the mto rule will be wrong.
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
return {
'route_ids': [(4, route.id) for route in reception_route | delivery_route | crossdock_route],
'mto_pull_id': mto_pull.id,
'reception_route_id': reception_route.id,
'delivery_route_id': delivery_route.id,
'crossdock_route_id': crossdock_route.id,
'mto_pull_id': {
'depends': ['delivery_steps'],
'create_values': {
'active': True,
'procure_method': 'make_to_order',
'company_id': self.company_id.id,
'action': 'pull',
'auto': 'manual',
'propagate': True,
'route_id': self._find_global_route('stock.route_warehouse0_mto', 'Make To Order').id
},
'update_values': {
'name': self._format_rulename(location_id, location_dest_id, 'MTO'),
'location_id': location_dest_id.id,
'location_src_id': location_id.id,
'picking_type_id': picking_type_id.id,
}
}
}
def _create_or_update_route(self):
""" Create or update the warehouse's routes.
_get_routes_values method return a dict with:
- route field name (e.g: crossdock_route_id).
- field that trigger an update on the route (key 'depends').
- routing_key used in order to find rules contained in the route.
- create values.
- update values when a field in depends is modified.
- rules default values.
This method do an iteration on each route returned and update/create
them. In order to update the rules contained in the route it will
use the get_rules_dict that return a dict:
- a receptions/delivery,... step value as key (e.g 'pick_ship')
- a list of routing object that represents the rules needed to
fullfil the pupose of the route.
The routing_key from _get_routes_values is match with the get_rules_dict
key in order to create/update the rules in the route
(_find_existing_rule_or_create method is responsible for this part).
"""
# Create routes and active/create their related rules.
routes = []
rules_dict = self.get_rules_dict()
for route_field, route_data in self._get_routes_values().items():
# If the route exists update it
if self[route_field]:
route = self[route_field]
route.write(route_data['route_update_values'])
route.rule_ids.write({'active': False})
# Create the route
else:
route_data['route_create_values'].update(route_data['route_update_values'])
route = self.env['stock.location.route'].create(route_data['route_create_values'])
self[route_field] = route
# Get rules needed for the route
routing_key = route_data.get('routing_key')
rules = rules_dict[self.id][routing_key]
if 'rules_values' in route_data:
route_data['rules_values'].update({'route_id': route.id})
else:
route_data['rules_values'] = {'route_id': route.id}
rules_list = self._get_rule_values(
rules, values=route_data['rules_values'])
# Create/Active rules
self._find_existing_rule_or_create(rules_list)
if route_data['route_create_values'].get('warehouse_selectable', False) or route_data['route_update_values'].get('warehouse_selectable', False):
routes.append(self[route_field])
return {
'route_ids': [(4, route.id) for route in routes],
}
def _get_routes_values(self):
""" Return information in order to update warehouse routes.
- The key is a route field sotred as a Many2one on the warehouse
- This key contains a dict with route values:
- routing_key: a key used in order to match rules from
get_rules_dict function. It would be usefull in order to generate
the route's rules.
- route_create_values: When the Many2one does not exist the route
is created based on values contained in this dict.
- route_update_values: When a field contained in 'depends' key is
modified and the Many2one exist on the warehouse, the route will be
update with the values contained in this dict.
- rules_values: values added to the routing in order to create the
route's rules.
"""
return {
'reception_route_id': {
'routing_key': self.reception_steps,
'depends': ['reception_steps'],
'route_update_values': {
'name': self._format_routename(route_type=self.reception_steps)
},
'route_create_values': {
'product_categ_selectable': True,
'warehouse_selectable': True,
'product_selectable': False,
'company_id': self.company_id.id,
'sequence': 10,
},
'rules_values': {
'active': True,
'procure_method': 'make_to_order'
}
},
'delivery_route_id': {
'routing_key': self.delivery_steps,
'depends': ['delivery_steps'],
'route_update_values': {
'name': self._format_routename(route_type=self.delivery_steps)
},
'route_create_values': {
'product_categ_selectable': True,
'warehouse_selectable': True,
'product_selectable': False,
'company_id': self.company_id.id,
'sequence': 10,
},
'rules_values': {
'active': True,
}
},
'crossdock_route_id': {
'routing_key': 'crossdock',
'depends': ['delivery_steps', 'reception_steps'],
'route_update_values': {
'name': self._format_routename(route_type='crossdock'),
'active': self.reception_steps != 'one_step' and self.delivery_steps != 'ship_only'
},
'route_create_values': {
'warehouse_selectable': True,
'product_selectable': True,
'product_categ_selectable': True,
'active': self.delivery_steps != 'ship_only' and self.reception_steps != 'one_step',
'company_id': self.company_id.id,
'sequence': 20,
},
'rules_values': {
'active': True,
'procure_method': 'make_to_order'
}
}
}
def _find_existing_rule_or_create(self, rules_list):
""" This method will find existing rule or create new one"""
""" This method will find existing rules or create new one. """
for rule_vals in rules_list:
existing_rule = self.env['stock.rule'].search([
('picking_type_id', '=', rule_vals['picking_type_id']),
@@ -280,70 +435,36 @@ class Warehouse(models.Model):
else:
existing_rule.write({'active': True})
def _create_or_update_reception_route(self, routes_data):
routes_data = routes_data or self.get_routes_dict()
for warehouse in self:
if warehouse.reception_route_id:
reception_route = warehouse.reception_route_id
reception_route.write({'name': warehouse._format_routename(route_type=warehouse.reception_steps)})
reception_route.rule_ids.write({'active': False})
else:
warehouse.reception_route_id = reception_route = self.env['stock.location.route'].create(warehouse._get_reception_delivery_route_values(warehouse.reception_steps))
# stock rules for reception
routings = routes_data[warehouse.id][warehouse.reception_steps]
rules_list = warehouse._get_rule_values(
routings, values={'active': True, 'procure_method': 'make_to_order', 'route_id': reception_route.id})
warehouse._find_existing_rule_or_create(rules_list)
return reception_route
def _get_locations_values(self, vals):
""" Update the warehouse locations. """
def_values = self.default_get(['reception_steps', 'delivery_steps'])
reception_steps = vals.get('reception_steps', def_values['reception_steps'])
delivery_steps = vals.get('delivery_steps', def_values['delivery_steps'])
sub_locations = {
'lot_stock_id': {'name': _('Stock'), 'active': True, 'usage': 'internal'},
'wh_input_stock_loc_id': {'name': _('Input'), 'active': reception_steps != 'one_step', 'usage': 'internal'},
'wh_qc_stock_loc_id': {'name': _('Quality Control'), 'active': reception_steps == 'three_steps', 'usage': 'internal'},
'wh_output_stock_loc_id': {'name': _('Output'), 'active': delivery_steps != 'ship_only', 'usage': 'internal'},
'wh_pack_stock_loc_id': {'name': _('Packing Zone'), 'active': delivery_steps == 'pick_pack_ship', 'usage': 'internal'},
}
return sub_locations
def _create_or_update_delivery_route(self, routes_data):
""" Delivery (MTS) route """
routes_data = routes_data or self.get_routes_dict()
def _create_missing_locations(self, vals):
""" It could happen that the user delete a mandatory location or a
module with new locations was installed after some warehouses creation.
In this case, this function will create missing locations in order to
avoid mistakes during picking types and rules creation.
"""
sub_locations = self._get_locations_values(vals)
for warehouse in self:
if warehouse.delivery_route_id:
delivery_route = warehouse.delivery_route_id
delivery_route.write({'name': warehouse._format_routename(route_type=warehouse.delivery_steps)})
delivery_route.rule_ids.write({'active': False})
else:
delivery_route = self.env['stock.location.route'].create(warehouse._get_reception_delivery_route_values(warehouse.delivery_steps))
# stock (pull) rules for delivery
routings = routes_data[warehouse.id][warehouse.delivery_steps]
rules_list = warehouse._get_rule_values(
routings, values={'active': True, 'route_id': delivery_route.id})
warehouse._find_existing_rule_or_create(rules_list)
return delivery_route
def _create_or_update_mto_pull(self, routes_data):
""" Create MTO stock rule and link it to the generic MTO route """
routes_data = routes_data or self.get_routes_dict()
Rule = self.env['stock.rule']
for warehouse in self:
routings = routes_data[warehouse.id][warehouse.delivery_steps]
if warehouse.mto_pull_id:
mto_pull = warehouse.mto_pull_id
mto_pull.write(warehouse._get_mto_rules_values(routings)[0])
else:
mto_pull = Rule.create(warehouse._get_mto_rules_values(routings)[0])
return mto_pull
def _create_or_update_crossdock_route(self, routes_data):
""" Create or update the cross dock operations route, that can be set on
products and product categories """
routes_data = routes_data or self.get_routes_dict()
for warehouse in self:
if warehouse.crossdock_route_id:
crossdock_route = warehouse.crossdock_route_id
crossdock_route.write({'active': warehouse.reception_steps != 'one_step' and warehouse.delivery_steps != 'ship_only'})
else:
crossdock_route = self.env['stock.location.route'].create(warehouse._get_crossdock_route_values())
# note: fixed cross-dock is logically mto
routings = routes_data[warehouse.id]['crossdock']
pull_list = warehouse._get_rule_values(
routings,
values={'procure_method': 'make_to_order', 'active': warehouse.delivery_steps != 'ship_only' and warehouse.reception_steps != 'one_step', 'route_id': crossdock_route.id})
for rule_vals in pull_list:
self.env['stock.rule'].create(rule_vals)
return crossdock_route
missing_location = {}
for location, location_values in sub_locations.items():
if not warehouse[location] and location not in vals:
location_values['location_id'] = vals.get('view_location_id', warehouse.view_location_id.id)
location_values['company_id'] = vals.get('company_id', warehouse.company_id.id)
missing_location[location] = self.env['stock.location'].create(location_values).id
if missing_location:
warehouse.write(missing_location)
def create_resupply_routes(self, supplier_warehouses):
Route = self.env['stock.location.route']
@@ -359,8 +480,11 @@ class Warehouse(models.Model):
output_location = supplier_wh.lot_stock_id if supplier_wh.delivery_steps == 'ship_only' else supplier_wh.wh_output_stock_loc_id
# Create extra MTO rule (only for 'ship only' because in the other cases MTO rules already exists)
if supplier_wh.delivery_steps == 'ship_only':
Rule.create(supplier_wh._get_mto_rules_values([
self.Routing(output_location, transit_location, supplier_wh.out_type_id, 'pull')])[0])
routing = [self.Routing(output_location, transit_location, supplier_wh.out_type_id, 'pull')]
mto_vals = supplier_wh._get_global_route_rules_values().get('mto_pull_id')
values = mto_vals['create_values']
mto_rule_val = supplier_wh._get_rule_values(routing, values, name_suffix='MTO')
Rule.create(mto_rule_val[0])
inter_wh_route = Route.create(self._get_inter_warehouse_route_values(supplier_wh))
@@ -404,49 +528,33 @@ class Warehouse(models.Model):
'pick_pack_ship': _('Deliver in 3 steps (pick + pack + ship)')}
return names[route_type]
def get_routes_dict(self):
# TDE todo: rename me (was get_routes_dict)
def get_rules_dict(self):
""" Define the rules source/destination locations, picking_type and
action needed for each warehouse route configuration.
"""
customer_loc, supplier_loc = self._get_partner_locations()
return dict((warehouse.id, {
'one_step': [],
'two_steps': [self.Routing(warehouse.wh_input_stock_loc_id, warehouse.lot_stock_id, warehouse.int_type_id, 'pull_push')],
'three_steps': [
self.Routing(warehouse.wh_input_stock_loc_id, warehouse.wh_qc_stock_loc_id, warehouse.int_type_id, 'pull_push'),
self.Routing(warehouse.wh_qc_stock_loc_id, warehouse.lot_stock_id, warehouse.int_type_id, 'pull_push')],
'crossdock': [
self.Routing(warehouse.wh_input_stock_loc_id, warehouse.wh_output_stock_loc_id, warehouse.int_type_id, 'pull'),
self.Routing(warehouse.wh_output_stock_loc_id, customer_loc, warehouse.out_type_id, 'pull')],
'ship_only': [self.Routing(warehouse.lot_stock_id, customer_loc, warehouse.out_type_id, 'pull')],
'pick_ship': [
self.Routing(warehouse.lot_stock_id, warehouse.wh_output_stock_loc_id, warehouse.pick_type_id, 'pull'),
self.Routing(warehouse.wh_output_stock_loc_id, customer_loc, warehouse.out_type_id, 'pull')],
'pick_pack_ship': [
self.Routing(warehouse.lot_stock_id, warehouse.wh_pack_stock_loc_id, warehouse.pick_type_id, 'pull'),
self.Routing(warehouse.wh_pack_stock_loc_id, warehouse.wh_output_stock_loc_id, warehouse.pack_type_id,'pull'),
self.Routing(warehouse.wh_output_stock_loc_id, customer_loc, warehouse.out_type_id, 'pull')],
'company_id': warehouse.company_id.id,
}) for warehouse in self)
def _get_reception_delivery_route_values(self, route_type):
return {
'name': self._format_routename(route_type=route_type),
'product_categ_selectable': True,
'warehouse_selectable': True,
'product_selectable': False,
'company_id': self.company_id.id,
'sequence': 10,
warehouse.id: {
'one_step': [],
'two_steps': [self.Routing(warehouse.wh_input_stock_loc_id, warehouse.lot_stock_id, warehouse.int_type_id, 'pull_push')],
'three_steps': [
self.Routing(warehouse.wh_input_stock_loc_id, warehouse.wh_qc_stock_loc_id, warehouse.int_type_id, 'pull_push'),
self.Routing(warehouse.wh_qc_stock_loc_id, warehouse.lot_stock_id, warehouse.int_type_id, 'pull_push')],
'crossdock': [
self.Routing(warehouse.wh_input_stock_loc_id, warehouse.wh_output_stock_loc_id, warehouse.int_type_id, 'pull'),
self.Routing(warehouse.wh_output_stock_loc_id, customer_loc, warehouse.out_type_id, 'pull')],
'ship_only': [self.Routing(warehouse.lot_stock_id, customer_loc, warehouse.out_type_id, 'pull')],
'pick_ship': [
self.Routing(warehouse.lot_stock_id, warehouse.wh_output_stock_loc_id, warehouse.pick_type_id, 'pull'),
self.Routing(warehouse.wh_output_stock_loc_id, customer_loc, warehouse.out_type_id, 'pull')],
'pick_pack_ship': [
self.Routing(warehouse.lot_stock_id, warehouse.wh_pack_stock_loc_id, warehouse.pick_type_id, 'pull'),
self.Routing(warehouse.wh_pack_stock_loc_id, warehouse.wh_output_stock_loc_id, warehouse.pack_type_id, 'pull'),
self.Routing(warehouse.wh_output_stock_loc_id, customer_loc, warehouse.out_type_id, 'pull')],
'company_id': warehouse.company_id.id,
} for warehouse in self
}
@api.model
@api.returns('stock.location.route', lambda value: value.id)
def _get_mto_route(self):
mto_route = self.env.ref('stock.route_warehouse0_mto', raise_if_not_found=False)
if not mto_route:
mto_route = self.env['stock.location.route'].search([('name', 'like', _('Make To Order'))], limit=1)
if not mto_route:
raise UserError(_('Can\'t find any generic Make To Order route.'))
return mto_route
def _get_inter_warehouse_route_values(self, supplier_warehouse):
return {
'name': _('%s: Supply Product from %s') % (self.name, supplier_warehouse.name),
@@ -458,21 +566,9 @@ class Warehouse(models.Model):
'company_id': self.company_id.id,
}
def _get_crossdock_route_values(self):
return {
'name': self._format_routename(route_type='crossdock'),
'warehouse_selectable': True,
'product_selectable': True,
'product_categ_selectable': True,
'active': self.delivery_steps != 'ship_only' and self.reception_steps != 'one_step',
'company_id': self.company_id.id,
'sequence': 20,
}
# Pull / Push tools
# ------------------------------------------------------------
def _get_rule_values(self, route_values, values=None, name_suffix=''):
first_rule = True
rules_list = []
@@ -484,7 +580,7 @@ class Warehouse(models.Model):
'action': routing.action,
'auto': 'manual',
'picking_type_id': routing.picking_type.id,
'procure_method': first_rule is True and 'make_to_stock' or 'make_to_order',
'procure_method': first_rule and 'make_to_stock' or 'make_to_order',
'warehouse_id': self.id,
'company_id': self.company_id.id,
'propagate': routing.picking_type != self.pick_type_id,
@@ -494,14 +590,6 @@ class Warehouse(models.Model):
first_rule = False
return rules_list
def _get_mto_rules_values(self, route_values):
mto_route = self._get_mto_route()
rules_list = self._get_rule_values(route_values, values={
'route_id': mto_route.id,
'procure_method': 'make_to_order',
'active': True}, name_suffix=_('MTO'))
return rules_list
def _get_supply_pull_rules_values(self, route_values, values=None):
pull_values = {}
pull_values.update(values)
@@ -539,7 +627,7 @@ class Warehouse(models.Model):
else:
# We need to delete all the MTO stock rules, otherwise they risk to be used in the system
Rule.search([
'&', ('route_id', '=', self._get_mto_route().id),
'&', ('route_id', '=', self._find_global_route('stock.route_warehouse0_mto', 'Make To Order').id),
('location_id.usage', '=', 'transit'),
('action', '!=', 'push'),
('location_src_id', '=', self.lot_stock_id.id)]).write({'active': False})
@@ -556,29 +644,6 @@ class Warehouse(models.Model):
('location_src_id.usage', '=', 'transit')
]).write({'location_id': new_location.id})
def _update_routes(self):
routes_data = self.get_routes_dict()
# change the default source and destination location and (de)activate operation types
self._update_picking_type()
delivery_route = self._create_or_update_delivery_route(routes_data)
reception_route = self._create_or_update_reception_route(routes_data)
crossdock_route = self._create_or_update_crossdock_route(routes_data)
mto_pull = self._create_or_update_mto_pull(routes_data)
return {
'route_ids': [(4, route.id) for route in reception_route | delivery_route | crossdock_route],
'mto_pull_id': mto_pull.id,
'reception_route_id': reception_route.id,
'delivery_route_id': delivery_route.id,
'crossdock_route_id': crossdock_route.id,
}
@api.one
def _update_picking_type(self):
picking_type_values = self._get_picking_type_values(self.reception_steps, self.delivery_steps, self.wh_pack_stock_loc_id)
for field_name, values in picking_type_values.items():
self[field_name].write(values)
def _update_name_and_code(self, new_name=False, new_code=False):
if new_code:
self.mapped('lot_stock_id').mapped('location_id').write({'name': new_code})
@@ -635,19 +700,74 @@ class Warehouse(models.Model):
# Misc
# ------------------------------------------------------------
def _get_picking_type_values(self, reception_steps, delivery_steps, pack_stop_location):
input_loc, output_loc = self._get_input_output_locations(reception_steps, delivery_steps)
def _get_picking_type_update_values(self):
""" Return values in order to update the existing picking type when the
warehouse's delivery_steps or reception_steps are modify.
"""
input_loc, output_loc = self._get_input_output_locations(self.reception_steps, self.delivery_steps)
return {
'in_type_id': {'default_location_dest_id': input_loc.id},
'out_type_id': {'default_location_src_id': output_loc.id},
'pick_type_id': {
'active': delivery_steps != 'ship_only',
'default_location_dest_id': output_loc.id if delivery_steps == 'pick_ship' else pack_stop_location.id},
'pack_type_id': {'active': delivery_steps == 'pick_pack_ship'},
'active': self.delivery_steps != 'ship_only',
'default_location_dest_id': output_loc.id if self.delivery_steps == 'pick_ship' else self.wh_pack_stock_loc_id.id},
'pack_type_id': {'active': self.delivery_steps == 'pick_pack_ship'},
'int_type_id': {},
}
def _get_picking_type_create_values(self, max_sequence):
""" When a warehouse is created this method return the values needed in
order to create the new picking types for this warehouse. Every picking
type are created at the same time than the warehouse howver they are
activated or archived depending the delivery_steps or reception_steps.
"""
input_loc, output_loc = self._get_input_output_locations(self.reception_steps, self.delivery_steps)
return {
'in_type_id': {
'name': _('Receipts'),
'code': 'incoming',
'use_create_lots': True,
'use_existing_lots': False,
'default_location_src_id': False,
'sequence': max_sequence + 1,
}, 'out_type_id': {
'name': _('Delivery Orders'),
'code': 'outgoing',
'use_create_lots': False,
'use_existing_lots': True,
'default_location_dest_id': False,
'sequence': max_sequence + 5,
}, 'pack_type_id': {
'name': _('Pack'),
'code': 'internal',
'use_create_lots': False,
'use_existing_lots': True,
'default_location_src_id': self.wh_pack_stock_loc_id.id,
'default_location_dest_id': output_loc.id,
'sequence': max_sequence + 4,
}, 'pick_type_id': {
'name': _('Pick'),
'code': 'internal',
'use_create_lots': False,
'use_existing_lots': True,
'default_location_src_id': self.lot_stock_id.id,
'sequence': max_sequence + 3,
}, 'int_type_id': {
'name': _('Internal Transfers'),
'code': 'internal',
'use_create_lots': False,
'use_existing_lots': True,
'default_location_src_id': self.lot_stock_id.id,
'default_location_dest_id': self.lot_stock_id.id,
'active': self.reception_steps != 'one_step' or self.delivery_steps != 'ship_only' or self.user_has_groups('stock.group_stock_multi_locations'),
'sequence': max_sequence + 2,
},
}, max_sequence + 6
def _get_sequence_values(self):
""" Each picking type is created with a sequence. This method returns
the sequence values associated to each picking type.
"""
return {
'in_type_id': {
'name': self.name + ' ' + _('Sequence in'),
@@ -677,7 +797,9 @@ class Warehouse(models.Model):
}
def _format_rulename(self, from_loc, dest_loc, suffix):
rulename = '%s: %s → %s' % (self.code, from_loc.name, dest_loc.name)
rulename = '%s: %s' % (self.code, from_loc.name)
if dest_loc:
rulename += ' → %s' % (dest_loc.name)
if suffix:
rulename += ' (' + suffix + ')'
return rulename
-1
View File
@@ -332,7 +332,6 @@ class TestWarehouse(TestStockCommon):
route_shop_namur = warehouse_shop_namur.resupply_route_ids
route_shop_wavre = warehouse_shop_wavre.resupply_route_ids
# The product contains the 2 resupply routes.
product = self.env['product.product'].create({
'name': 'Fakir',
+3 -2
View File
@@ -35,9 +35,10 @@
<field name="reception_steps" widget='radio'/>
<field name="delivery_steps" widget='radio'/>
</group>
<group string="Resupply" groups="stock.group_stock_multi_warehouses">
<field name="show_resupply" invisible="1"/>
<group name="group_resupply" string="Resupply" attrs="{'invisible': [('show_resupply', '!=', True)]}">
<field name="warehouse_count" invisible="1"/>
<field name="resupply_wh_ids" domain="[('id', '!=', id)]" widget='many2many_checkboxes' attrs="{'invisible': [('warehouse_count', '&lt;', 1)]}"/>
<field name="resupply_wh_ids" attrs="{'invisible': [('show_resupply', '!=', True)]}" domain="[('id', '!=', id)]" widget='many2many_checkboxes'/>
</group>
</group>
</page>