[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:
@@ -4,3 +4,15 @@
|
|||||||
from . import models
|
from . import models
|
||||||
from . import wizard
|
from . import wizard
|
||||||
from . import report
|
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})
|
||||||
|
|||||||
@@ -46,4 +46,5 @@
|
|||||||
],
|
],
|
||||||
'test': [],
|
'test': [],
|
||||||
'application': True,
|
'application': True,
|
||||||
|
'post_init_hook': '_create_warehouse_data',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,14 +83,6 @@
|
|||||||
<field name="sequence">0</field>
|
<field name="sequence">0</field>
|
||||||
</record>
|
</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>
|
</data>
|
||||||
|
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|||||||
@@ -164,6 +164,31 @@ class MrpProduction(models.Model):
|
|||||||
is_locked = fields.Boolean('Is Locked', default=True, copy=False)
|
is_locked = fields.Boolean('Is Locked', default=True, copy=False)
|
||||||
show_final_lots = fields.Boolean('Show Final Lots', compute='_compute_show_lots')
|
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')
|
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')
|
@api.depends('product_uom_id', 'product_qty', 'product_id.uom_id')
|
||||||
def _compute_product_uom_qty(self):
|
def _compute_product_uom_qty(self):
|
||||||
@@ -433,12 +458,12 @@ class MrpProduction(models.Model):
|
|||||||
@api.multi
|
@api.multi
|
||||||
def _adjust_procure_method(self):
|
def _adjust_procure_method(self):
|
||||||
try:
|
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:
|
except:
|
||||||
mto_route = False
|
mto_route = False
|
||||||
for move in self.move_raw_ids:
|
for move in self.move_raw_ids:
|
||||||
product = move.product_id
|
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?
|
# 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),
|
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)
|
('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'))
|
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'))
|
raw_moves = production.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel'))
|
||||||
(finish_moves | raw_moves)._action_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})
|
self.write({'state': 'cancel', 'is_locked': True})
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ class StockRule(models.Model):
|
|||||||
'product_id': product_id.id,
|
'product_id': product_id.id,
|
||||||
'product_qty': product_qty,
|
'product_qty': product_qty,
|
||||||
'product_uom_id': product_uom.id,
|
'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,
|
'location_dest_id': location_id.id,
|
||||||
'bom_id': bom.id,
|
'bom_id': bom.id,
|
||||||
'date_planned_start': fields.Datetime.to_string(self._get_date_planned(product_id, values)),
|
'date_planned_start': fields.Datetime.to_string(self._get_date_planned(product_id, values)),
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# 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):
|
class StockWarehouse(models.Model):
|
||||||
@@ -12,103 +13,225 @@ class StockWarehouse(models.Model):
|
|||||||
help="When products are manufactured, they can be manufactured in this warehouse.")
|
help="When products are manufactured, they can be manufactured in this warehouse.")
|
||||||
manufacture_pull_id = fields.Many2one(
|
manufacture_pull_id = fields.Many2one(
|
||||||
'stock.rule', 'Manufacture Rule')
|
'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(
|
manu_type_id = fields.Many2one(
|
||||||
'stock.picking.type', 'Manufacturing Operation Type',
|
'stock.picking.type', 'Manufacturing Operation Type',
|
||||||
domain=[('code', '=', 'mrp_operation')])
|
domain=[('code', '=', 'mrp_operation')])
|
||||||
|
|
||||||
def create_sequences_and_picking_types(self):
|
pbm_type_id = fields.Many2one('stock.picking.type', 'Picking Before Manufacturing Operation Type')
|
||||||
res = super(StockWarehouse, self).create_sequences_and_picking_types()
|
sam_type_id = fields.Many2one('stock.picking.type', 'Stock After Manufacturing Operation Type')
|
||||||
self._create_manufacturing_picking_type()
|
|
||||||
return res
|
|
||||||
|
|
||||||
@api.multi
|
manufacture_steps = fields.Selection([
|
||||||
def get_routes_dict(self):
|
('mrp_one_step', 'Manufacture (1 step)'),
|
||||||
result = super(StockWarehouse, self).get_routes_dict()
|
('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:
|
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
|
return result
|
||||||
|
|
||||||
def _get_manufacture_route_id(self):
|
@api.model
|
||||||
manufacture_route = self.env.ref('mrp.route_warehouse0_manufacture', raise_if_not_found=False)
|
def _get_production_location(self):
|
||||||
if not manufacture_route:
|
location = self.env.ref('stock.location_production', raise_if_not_found=False)
|
||||||
manufacture_route = self.env['stock.location.route'].search([('name', 'like', _('Manufacture'))], limit=1)
|
if not location:
|
||||||
if not manufacture_route:
|
location = self.env['stock.location'].search([('usage', '=', 'production')], limit=1)
|
||||||
raise exceptions.UserError(_('Can\'t find any generic Manufacture route.'))
|
if not location:
|
||||||
return manufacture_route.id
|
raise UserError(_('Can\'t find any production location.'))
|
||||||
|
return location
|
||||||
|
|
||||||
def _get_manufacture_pull_rules_values(self, route_values):
|
def _get_routes_values(self):
|
||||||
if not self.manu_type_id:
|
routes = super(StockWarehouse, self)._get_routes_values()
|
||||||
self._create_manufacturing_picking_type()
|
routes.update({
|
||||||
pull_rules_list = self._get_rule_values(route_values, values={
|
'pbm_route_id': {
|
||||||
'name': self._format_routename(_(' Manufacture')),
|
'routing_key': self.manufacture_steps,
|
||||||
'location_src_id': False, # TDE FIXME
|
'depends': ['manufacture_steps', 'manufacture_to_resupply'],
|
||||||
'action': 'manufacture',
|
'route_update_values': {
|
||||||
'route_id': self._get_manufacture_route_id(),
|
'name': self._format_routename(route_type=self.manufacture_steps),
|
||||||
'picking_type_id': self.manu_type_id.id,
|
'active': self.manufacture_steps != 'mrp_one_step',
|
||||||
'propagate': False,
|
},
|
||||||
'active': True})
|
'route_create_values': {
|
||||||
return pull_rules_list
|
'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):
|
def _get_route_name(self, route_type):
|
||||||
# TDE CLEANME
|
names = {
|
||||||
picking_type_obj = self.env['stock.picking.type']
|
'mrp_one_step': _('Manufacture (1 step)'),
|
||||||
seq_obj = self.env['ir.sequence']
|
'pbm': _('Pick components and then manufacture'),
|
||||||
for warehouse in self:
|
'pbm_sam': _('Pick components, manufacture and then store products (3 steps)'),
|
||||||
#man_seq_id = seq_obj.sudo().create('name': warehouse.name + _(' Sequence Manufacturing'), 'prefix': warehouse.code + '/MANU/', 'padding')
|
}
|
||||||
wh_stock_loc = warehouse.lot_stock_id
|
if route_type in names:
|
||||||
seq = seq_obj.search([('code', '=', 'mrp.production')], limit=1)
|
return names[route_type]
|
||||||
other_pick_type = picking_type_obj.search([('warehouse_id', '=', warehouse.id)], order = 'sequence desc', limit=1)
|
else:
|
||||||
color = other_pick_type.color if other_pick_type else 0
|
return super(StockWarehouse, self)._get_route_name(route_type)
|
||||||
max_sequence = other_pick_type and other_pick_type.sequence or 0
|
|
||||||
manu_type = picking_type_obj.create({
|
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'),
|
'name': _('Manufacturing'),
|
||||||
'warehouse_id': warehouse.id,
|
|
||||||
'code': 'mrp_operation',
|
'code': 'mrp_operation',
|
||||||
'use_create_lots': True,
|
'use_create_lots': True,
|
||||||
'use_existing_lots': False,
|
'use_existing_lots': True,
|
||||||
'sequence_id': seq.id,
|
'sequence': next_sequence + 2
|
||||||
'default_location_src_id': wh_stock_loc.id,
|
},
|
||||||
'default_location_dest_id': wh_stock_loc.id,
|
})
|
||||||
'sequence': max_sequence,
|
return data, max_sequence + 4
|
||||||
'color': color})
|
|
||||||
warehouse.write({'manu_type_id': manu_type.id})
|
|
||||||
|
|
||||||
def _create_or_update_manufacture_pull(self, routes_data):
|
def _get_picking_type_update_values(self):
|
||||||
routes_data = routes_data or self.get_routes_dict()
|
data = super(StockWarehouse, self)._get_picking_type_update_values()
|
||||||
for warehouse in self:
|
data.update({
|
||||||
routings = routes_data[warehouse.id]['manufacture']
|
'pbm_type_id': {'active': self.manufacture_to_resupply and self.manufacture_steps in ('pbm', 'pbm_sam')},
|
||||||
if warehouse.manufacture_pull_id:
|
'sam_type_id': {'active': self.manufacture_to_resupply and self.manufacture_steps == 'pbm_sam'},
|
||||||
manufacture_pull = warehouse.manufacture_pull_id
|
'manu_type_id': {
|
||||||
manufacture_pull.write(warehouse._get_manufacture_pull_rules_values(routings)[0])
|
'active': self.manufacture_to_resupply,
|
||||||
else:
|
'default_location_src_id': self.manufacture_steps in ('pbm', 'pbm_sam') and self.pbm_loc_id.id or self.lot_stock_id.id,
|
||||||
manufacture_pull = self.env['stock.rule'].create(warehouse._get_manufacture_pull_rules_values(routings)[0])
|
'default_location_dest_id': self.manufacture_steps == 'pbm_sam' and self.sam_loc_id or self.lot_stock_id.id,
|
||||||
return manufacture_pull
|
},
|
||||||
|
})
|
||||||
@api.multi
|
return data
|
||||||
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
|
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def write(self, vals):
|
def write(self, vals):
|
||||||
if 'manufacture_to_resupply' in vals:
|
if any(field in vals for field in ('manufacture_steps', 'manufacture_to_resupply')):
|
||||||
if vals.get("manufacture_to_resupply"):
|
for warehouse in self:
|
||||||
for warehouse in self.filtered(lambda warehouse: not warehouse.manufacture_pull_id):
|
warehouse._update_location_manufacture(vals.get('manufacture_steps', warehouse.manufacture_steps))
|
||||||
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()
|
|
||||||
return super(StockWarehouse, self).write(vals)
|
return super(StockWarehouse, self).write(vals)
|
||||||
|
|
||||||
@api.multi
|
@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')
|
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
|
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
|
@api.multi
|
||||||
def _update_name_and_code(self, name=False, code=False):
|
def _update_name_and_code(self, name=False, code=False):
|
||||||
res = super(StockWarehouse, self)._update_name_and_code(name, code)
|
res = super(StockWarehouse, self)._update_name_and_code(name, code)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
from . import test_bom
|
from . import test_bom
|
||||||
from . import test_order
|
from . import test_order
|
||||||
from . import test_stock
|
from . import test_stock
|
||||||
|
from . import test_warehouse_multistep_manufacturing
|
||||||
from . import test_workorder_operation
|
from . import test_workorder_operation
|
||||||
from . import test_procurement
|
from . import test_procurement
|
||||||
from . import test_unbuild
|
from . import test_unbuild
|
||||||
|
|||||||
@@ -66,7 +66,10 @@ class TestBoM(TestMrpCommon):
|
|||||||
tmp_picking_type = self.env['stock.picking.type'].create({
|
tmp_picking_type = self.env['stock.picking.type'].create({
|
||||||
'name': 'Manufacturing',
|
'name': 'Manufacturing',
|
||||||
'code': 'mrp_operation',
|
'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({
|
test_bom_1 = self.env['mrp.bom'].create({
|
||||||
'product_tmpl_id': self.product_5.product_tmpl_id.id,
|
'product_tmpl_id': self.product_5.product_tmpl_id.id,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class TestWarehouse(common.TestMrpCommon):
|
|||||||
warehouse_1_stock_manager.write({
|
warehouse_1_stock_manager.write({
|
||||||
'manufacture_to_resupply': False
|
'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.assertFalse(self.warehouse_1.manu_type_id.active)
|
||||||
self.assertNotIn(manu_route, warehouse_1_stock_manager._get_all_routes())
|
self.assertNotIn(manu_route, warehouse_1_stock_manager._get_all_routes())
|
||||||
warehouse_1_stock_manager.write({
|
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>
|
</div>
|
||||||
</button>
|
</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 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)]}">
|
<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">
|
<div class="o_field_widget o_stat_info">
|
||||||
<span class="o_stat_value"><field name="scrap_count"/></span>
|
<span class="o_stat_value"><field name="scrap_count"/></span>
|
||||||
|
|||||||
@@ -1,17 +1,29 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<odoo>
|
<odoo>
|
||||||
<!-- Warehouse -->
|
<!-- Warehouse -->
|
||||||
<record id="view_warehouse_inherited" model="ir.ui.view">
|
<record id="view_warehouse_inherit_mrp" model="ir.ui.view">
|
||||||
<field name="name">Stock Warehouse Inherited</field>
|
<field name="name">Stock Warehouse Inherit MRP</field>
|
||||||
<field name="model">stock.warehouse</field>
|
<field name="model">stock.warehouse</field>
|
||||||
<field name="inherit_id" ref="stock.view_warehouse"/>
|
<field name="inherit_id" ref="stock.view_warehouse"/>
|
||||||
<field name="arch" type="xml">
|
<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_to_resupply" />
|
||||||
|
<field name="manufacture_steps" attrs="{'invisible': [('manufacture_to_resupply', '=', False)]}" widget="radio"/>
|
||||||
</xpath>
|
</xpath>
|
||||||
<xpath expr="//field[@name='out_type_id']" position="after">
|
<xpath expr="//field[@name='out_type_id']" position="after">
|
||||||
<field name="manu_type_id" readonly="True"/>
|
<field name="manu_type_id" readonly="True"/>
|
||||||
</xpath>
|
</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>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ class MrpProduction(models.Model):
|
|||||||
'warehouse_id': production.location_dest_id.get_warehouse().id,
|
'warehouse_id': production.location_dest_id.get_warehouse().id,
|
||||||
'origin': production.name,
|
'origin': production.name,
|
||||||
'unit_factor': qty1 / (production.product_qty - production.qty_produced),
|
'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
|
'subproduct_id': sub_product.id
|
||||||
}
|
}
|
||||||
move = Move.create(data)
|
move = Move.create(data)
|
||||||
|
|||||||
@@ -15,4 +15,4 @@ def _create_buy_rules(cr, registry):
|
|||||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||||
warehouse_ids = env['stock.warehouse'].search([('buy_pull_id', '=', False)])
|
warehouse_ids = env['stock.warehouse'].search([('buy_pull_id', '=', False)])
|
||||||
for warehouse_id in warehouse_ids:
|
for warehouse_id in warehouse_ids:
|
||||||
warehouse_id.buy_pull_id = warehouse_id._create_buy_rule()
|
warehouse_id._create_or_update_global_routes_rules()
|
||||||
|
|||||||
@@ -115,43 +115,25 @@ class StockWarehouse(models.Model):
|
|||||||
help="When products are bought, they can be delivered to this warehouse")
|
help="When products are bought, they can be delivered to this warehouse")
|
||||||
buy_pull_id = fields.Many2one('stock.rule', 'Buy rule')
|
buy_pull_id = fields.Many2one('stock.rule', 'Buy rule')
|
||||||
|
|
||||||
@api.multi
|
def _get_global_route_rules_values(self):
|
||||||
def _get_buy_pull_rule(self):
|
rules = super(StockWarehouse, self)._get_global_route_rules_values()
|
||||||
buy_route_id = self.env.ref('purchase_stock.route_warehouse0_buy', raise_if_not_found=False)
|
location_id = self.in_type_id.default_location_dest_id
|
||||||
if not buy_route_id:
|
rules.update({
|
||||||
buy_route_id = self.env['stock.location.route'].search([('name', 'like', _('Buy'))], limit=1)
|
'buy_pull_id': {
|
||||||
if not buy_route_id:
|
'depends': ['reception_steps', 'buy_to_resupply'],
|
||||||
raise UserError(_("Can't find any Buy route. Please create a route with the 'Buy' action for your receipts operation types."))
|
'create_values': {
|
||||||
|
'action': 'buy',
|
||||||
return {
|
'picking_type_id': self.in_type_id.id,
|
||||||
'name': self._format_routename(_(' Buy')),
|
'route_id': self._find_global_route('purchase.route_warehouse0_buy', 'Buy').id
|
||||||
'location_id': self.in_type_id.default_location_dest_id.id,
|
},
|
||||||
'route_id': buy_route_id.id,
|
'update_values': {
|
||||||
'action': 'buy',
|
'active': self.buy_to_resupply,
|
||||||
'picking_type_id': self.in_type_id.id,
|
'name': self._format_rulename(location_id, False, 'Buy'),
|
||||||
'warehouse_id': self.id,
|
'location_id': location_id.id,
|
||||||
'group_propagation_option': 'none',
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
@api.multi
|
return rules
|
||||||
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)
|
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def _get_all_routes(self):
|
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)})
|
warehouse.buy_pull_id.write({'name': warehouse.buy_pull_id.name.replace(warehouse.name, name, 1)})
|
||||||
return res
|
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):
|
class ReturnPicking(models.TransientModel):
|
||||||
_inherit = "stock.return.picking"
|
_inherit = "stock.return.picking"
|
||||||
|
|||||||
@@ -19,6 +19,9 @@
|
|||||||
<xpath expr="//field[@name='resupply_wh_ids']" position="before">
|
<xpath expr="//field[@name='resupply_wh_ids']" position="before">
|
||||||
<field name="buy_to_resupply" />
|
<field name="buy_to_resupply" />
|
||||||
</xpath>
|
</xpath>
|
||||||
|
<xpath expr="//group[@name='group_resupply']" position="attributes">
|
||||||
|
<attribute name="attrs">{}</attribute>
|
||||||
|
</xpath>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
|||||||
@@ -4,3 +4,4 @@
|
|||||||
from . import test_sale_mrp_flow
|
from . import test_sale_mrp_flow
|
||||||
from . import test_sale_mrp_lead_time
|
from . import test_sale_mrp_lead_time
|
||||||
from . import test_sale_mrp_procurement
|
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 !")
|
||||||
@@ -310,7 +310,7 @@ class SaleOrderLine(models.Model):
|
|||||||
'date_planned': date_planned,
|
'date_planned': date_planned,
|
||||||
'route_ids': self.route_id,
|
'route_ids': self.route_id,
|
||||||
'warehouse_id': self.order_id.warehouse_id or False,
|
'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"):
|
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)
|
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:
|
else:
|
||||||
mto_route = False
|
mto_route = False
|
||||||
try:
|
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:
|
except UserError:
|
||||||
# if route MTO not found in ir_model_data, we treat the product as in MTS
|
# if route MTO not found in ir_model_data, we treat the product as in MTS
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -45,13 +45,10 @@ class ProcurementGroup(models.Model):
|
|||||||
class StockRule(models.Model):
|
class StockRule(models.Model):
|
||||||
_inherit = 'stock.rule'
|
_inherit = 'stock.rule'
|
||||||
|
|
||||||
def _get_stock_move_values(self, product_id, product_qty, product_uom, location_id, name, origin, values, group_id):
|
def _get_custom_move_fields(self):
|
||||||
result = super(StockRule, self)._get_stock_move_values(product_id, product_qty, product_uom, location_id, name, origin, values, group_id)
|
fields = super(StockRule, self)._get_custom_move_fields()
|
||||||
if values.get('sale_line_id', False):
|
fields += ['sale_line_id', 'partner_id']
|
||||||
result['sale_line_id'] = values['sale_line_id']
|
return fields
|
||||||
if values.get('partner_dest_id'):
|
|
||||||
result['partner_id'] = values['partner_dest_id'].id
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class StockPicking(models.Model):
|
class StockPicking(models.Model):
|
||||||
|
|||||||
@@ -186,6 +186,12 @@ class StockRule(models.Model):
|
|||||||
move._action_confirm()
|
move._action_confirm()
|
||||||
return True
|
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):
|
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.
|
''' 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.
|
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
|
# 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
|
# a new move with the correct qty
|
||||||
qty_left = product_qty
|
qty_left = product_qty
|
||||||
return {
|
move_values = {
|
||||||
'name': name[:2000],
|
'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,
|
'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,
|
'product_id': product_id.id,
|
||||||
@@ -221,6 +227,10 @@ class StockRule(models.Model):
|
|||||||
'propagate': self.propagate,
|
'propagate': self.propagate,
|
||||||
'priority': values.get('priority', "1"),
|
'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):
|
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),
|
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),
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ class Warehouse(models.Model):
|
|||||||
'stock.location.route', 'supplied_wh_id', 'Resupply Routes',
|
'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")
|
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')
|
warehouse_count = fields.Integer(compute='_compute_warehouse_count')
|
||||||
|
show_resupply = fields.Boolean(compute="_compute_show_resupply")
|
||||||
_sql_constraints = [
|
_sql_constraints = [
|
||||||
('warehouse_name_uniq', 'unique(name, company_id)', 'The name of the warehouse must be unique per company!'),
|
('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!'),
|
('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:
|
for warehouse in self:
|
||||||
warehouse.warehouse_count = self.env['stock.warehouse'].search_count([('id', 'not in', warehouse.ids)])
|
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
|
@api.model
|
||||||
def create(self, vals):
|
def create(self, vals):
|
||||||
# create view location for warehouse then create all locations
|
# create view location for warehouse then create all locations
|
||||||
@@ -86,17 +91,8 @@ class Warehouse(models.Model):
|
|||||||
if vals.get('company_id'):
|
if vals.get('company_id'):
|
||||||
loc_vals['company_id'] = vals.get('company_id')
|
loc_vals['company_id'] = vals.get('company_id')
|
||||||
vals['view_location_id'] = self.env['stock.location'].create(loc_vals).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():
|
for field_name, values in sub_locations.items():
|
||||||
values['location_id'] = vals['view_location_id']
|
values['location_id'] = vals['view_location_id']
|
||||||
if vals.get('company_id'):
|
if vals.get('company_id'):
|
||||||
@@ -106,11 +102,18 @@ class Warehouse(models.Model):
|
|||||||
# actually create WH
|
# actually create WH
|
||||||
warehouse = super(Warehouse, self).create(vals)
|
warehouse = super(Warehouse, self).create(vals)
|
||||||
# create sequences and operation types
|
# 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 ?
|
warehouse.write(new_vals) # TDE FIXME: use super ?
|
||||||
# create routes and push/stock rules
|
# create routes and push/stock rules
|
||||||
route_vals = warehouse.create_routes()
|
route_vals = warehouse._create_or_update_route()
|
||||||
warehouse.write(route_vals)
|
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
|
# update partner data if partner assigned
|
||||||
if vals.get('partner_id'):
|
if vals.get('partner_id'):
|
||||||
self._update_partner_data(vals['partner_id'], vals.get('company_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'):
|
if vals.get('code') or vals.get('name'):
|
||||||
warehouses._update_name_and_code(vals.get('name'), vals.get('code'))
|
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'):
|
if vals.get('reception_steps'):
|
||||||
warehouses._update_location_reception(vals['reception_steps'])
|
warehouses._update_location_reception(vals['reception_steps'])
|
||||||
if vals.get('delivery_steps'):
|
if vals.get('delivery_steps'):
|
||||||
@@ -142,11 +146,25 @@ class Warehouse(models.Model):
|
|||||||
|
|
||||||
res = super(Warehouse, self).write(vals)
|
res = super(Warehouse, self).write(vals)
|
||||||
|
|
||||||
# check if we need to delete and recreate route
|
for warehouse in self:
|
||||||
if vals.get('reception_steps') or vals.get('delivery_steps'):
|
# check if we need to delete and recreate route
|
||||||
route_vals = warehouses._update_routes()
|
depends = [depend for depends in [value['depends'] for value in warehouse._get_routes_values().values()] for depend in depends]
|
||||||
if route_vals:
|
if any(depend in vals for depend in depends):
|
||||||
self.write(route_vals)
|
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'):
|
if vals.get('resupply_wh_ids') and not vals.get('resupply_route_ids'):
|
||||||
for warehouse in warehouses:
|
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
|
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})
|
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()
|
IrSequenceSudo = self.env['ir.sequence'].sudo()
|
||||||
PickingType = self.env['stock.picking.type']
|
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
|
# 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')]
|
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]
|
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
|
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 = {}
|
warehouse_data = {}
|
||||||
sequence_data = self._get_sequence_values()
|
sequence_data = self._get_sequence_values()
|
||||||
# tde todo: backport sequence fix
|
|
||||||
create_data = {
|
# suit for each warehouse: reception, internal, pick, pack, ship
|
||||||
'in_type_id': {
|
max_sequence = self.env['stock.picking.type'].search_read([('sequence', '!=', False)], ['sequence'], limit=1, order='sequence desc')
|
||||||
'name': _('Receipts'),
|
max_sequence = max_sequence and max_sequence[0]['sequence'] or 0
|
||||||
'code': 'incoming',
|
|
||||||
'use_create_lots': True,
|
data = self._get_picking_type_update_values()
|
||||||
'use_existing_lots': False,
|
create_data, max_sequence = self._get_picking_type_create_values(max_sequence)
|
||||||
'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])
|
|
||||||
|
|
||||||
for picking_type, values in data.items():
|
for picking_type, values in data.items():
|
||||||
sequence = IrSequenceSudo.create(sequence_data[picking_type])
|
if self[picking_type]:
|
||||||
values.update(warehouse_id=self.id, color=color, sequence_id=sequence.id)
|
self[picking_type].update(values)
|
||||||
warehouse_data[picking_type] = PickingType.create(values).id
|
else:
|
||||||
PickingType.browse(warehouse_data['out_type_id']).write({'return_picking_type_id': warehouse_data['in_type_id']})
|
data[picking_type].update(create_data[picking_type])
|
||||||
PickingType.browse(warehouse_data['in_type_id']).write({'return_picking_type_id': warehouse_data['out_type_id']})
|
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
|
return warehouse_data
|
||||||
|
|
||||||
def create_routes(self):
|
def _create_or_update_global_routes_rules(self):
|
||||||
self.ensure_one()
|
""" Some rules are not specific to a warehouse(e.g MTO, Buy, ...)
|
||||||
routes_data = self.get_routes_dict()
|
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)
|
def _find_global_route(self, xml_id, route_name):
|
||||||
delivery_route = self._create_or_update_delivery_route(routes_data)
|
""" return a route record set from an xml_id or its name. """
|
||||||
mto_pull = self._create_or_update_mto_pull(routes_data)
|
route = self.env.ref(xml_id, raise_if_not_found=False)
|
||||||
crossdock_route = self._create_or_update_crossdock_route(routes_data)
|
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
|
def _get_global_route_rules_values(self):
|
||||||
self.create_resupply_routes(self.resupply_wh_ids)
|
""" Method used by _create_or_update_global_routes_rules. It's
|
||||||
|
purpose is to return a dict with this format.
|
||||||
# return routes and mto stock rule to store on the warehouse
|
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 {
|
return {
|
||||||
'route_ids': [(4, route.id) for route in reception_route | delivery_route | crossdock_route],
|
'mto_pull_id': {
|
||||||
'mto_pull_id': mto_pull.id,
|
'depends': ['delivery_steps'],
|
||||||
'reception_route_id': reception_route.id,
|
'create_values': {
|
||||||
'delivery_route_id': delivery_route.id,
|
'active': True,
|
||||||
'crossdock_route_id': crossdock_route.id,
|
'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):
|
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:
|
for rule_vals in rules_list:
|
||||||
existing_rule = self.env['stock.rule'].search([
|
existing_rule = self.env['stock.rule'].search([
|
||||||
('picking_type_id', '=', rule_vals['picking_type_id']),
|
('picking_type_id', '=', rule_vals['picking_type_id']),
|
||||||
@@ -280,70 +435,36 @@ class Warehouse(models.Model):
|
|||||||
else:
|
else:
|
||||||
existing_rule.write({'active': True})
|
existing_rule.write({'active': True})
|
||||||
|
|
||||||
def _create_or_update_reception_route(self, routes_data):
|
def _get_locations_values(self, vals):
|
||||||
routes_data = routes_data or self.get_routes_dict()
|
""" Update the warehouse locations. """
|
||||||
for warehouse in self:
|
def_values = self.default_get(['reception_steps', 'delivery_steps'])
|
||||||
if warehouse.reception_route_id:
|
reception_steps = vals.get('reception_steps', def_values['reception_steps'])
|
||||||
reception_route = warehouse.reception_route_id
|
delivery_steps = vals.get('delivery_steps', def_values['delivery_steps'])
|
||||||
reception_route.write({'name': warehouse._format_routename(route_type=warehouse.reception_steps)})
|
sub_locations = {
|
||||||
reception_route.rule_ids.write({'active': False})
|
'lot_stock_id': {'name': _('Stock'), 'active': True, 'usage': 'internal'},
|
||||||
else:
|
'wh_input_stock_loc_id': {'name': _('Input'), 'active': reception_steps != 'one_step', 'usage': 'internal'},
|
||||||
warehouse.reception_route_id = reception_route = self.env['stock.location.route'].create(warehouse._get_reception_delivery_route_values(warehouse.reception_steps))
|
'wh_qc_stock_loc_id': {'name': _('Quality Control'), 'active': reception_steps == 'three_steps', 'usage': 'internal'},
|
||||||
# stock rules for reception
|
'wh_output_stock_loc_id': {'name': _('Output'), 'active': delivery_steps != 'ship_only', 'usage': 'internal'},
|
||||||
routings = routes_data[warehouse.id][warehouse.reception_steps]
|
'wh_pack_stock_loc_id': {'name': _('Packing Zone'), 'active': delivery_steps == 'pick_pack_ship', 'usage': 'internal'},
|
||||||
rules_list = warehouse._get_rule_values(
|
}
|
||||||
routings, values={'active': True, 'procure_method': 'make_to_order', 'route_id': reception_route.id})
|
return sub_locations
|
||||||
warehouse._find_existing_rule_or_create(rules_list)
|
|
||||||
return reception_route
|
|
||||||
|
|
||||||
def _create_or_update_delivery_route(self, routes_data):
|
def _create_missing_locations(self, vals):
|
||||||
""" Delivery (MTS) route """
|
""" It could happen that the user delete a mandatory location or a
|
||||||
routes_data = routes_data or self.get_routes_dict()
|
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:
|
for warehouse in self:
|
||||||
if warehouse.delivery_route_id:
|
missing_location = {}
|
||||||
delivery_route = warehouse.delivery_route_id
|
for location, location_values in sub_locations.items():
|
||||||
delivery_route.write({'name': warehouse._format_routename(route_type=warehouse.delivery_steps)})
|
if not warehouse[location] and location not in vals:
|
||||||
delivery_route.rule_ids.write({'active': False})
|
location_values['location_id'] = vals.get('view_location_id', warehouse.view_location_id.id)
|
||||||
else:
|
location_values['company_id'] = vals.get('company_id', warehouse.company_id.id)
|
||||||
delivery_route = self.env['stock.location.route'].create(warehouse._get_reception_delivery_route_values(warehouse.delivery_steps))
|
missing_location[location] = self.env['stock.location'].create(location_values).id
|
||||||
# stock (pull) rules for delivery
|
if missing_location:
|
||||||
routings = routes_data[warehouse.id][warehouse.delivery_steps]
|
warehouse.write(missing_location)
|
||||||
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
|
|
||||||
|
|
||||||
def create_resupply_routes(self, supplier_warehouses):
|
def create_resupply_routes(self, supplier_warehouses):
|
||||||
Route = self.env['stock.location.route']
|
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
|
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)
|
# Create extra MTO rule (only for 'ship only' because in the other cases MTO rules already exists)
|
||||||
if supplier_wh.delivery_steps == 'ship_only':
|
if supplier_wh.delivery_steps == 'ship_only':
|
||||||
Rule.create(supplier_wh._get_mto_rules_values([
|
routing = [self.Routing(output_location, transit_location, supplier_wh.out_type_id, 'pull')]
|
||||||
self.Routing(output_location, transit_location, supplier_wh.out_type_id, 'pull')])[0])
|
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))
|
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)')}
|
'pick_pack_ship': _('Deliver in 3 steps (pick + pack + ship)')}
|
||||||
return names[route_type]
|
return names[route_type]
|
||||||
|
|
||||||
def get_routes_dict(self):
|
def get_rules_dict(self):
|
||||||
# TDE todo: rename me (was get_routes_dict)
|
""" Define the rules source/destination locations, picking_type and
|
||||||
|
action needed for each warehouse route configuration.
|
||||||
|
"""
|
||||||
customer_loc, supplier_loc = self._get_partner_locations()
|
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 {
|
return {
|
||||||
'name': self._format_routename(route_type=route_type),
|
warehouse.id: {
|
||||||
'product_categ_selectable': True,
|
'one_step': [],
|
||||||
'warehouse_selectable': True,
|
'two_steps': [self.Routing(warehouse.wh_input_stock_loc_id, warehouse.lot_stock_id, warehouse.int_type_id, 'pull_push')],
|
||||||
'product_selectable': False,
|
'three_steps': [
|
||||||
'company_id': self.company_id.id,
|
self.Routing(warehouse.wh_input_stock_loc_id, warehouse.wh_qc_stock_loc_id, warehouse.int_type_id, 'pull_push'),
|
||||||
'sequence': 10,
|
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):
|
def _get_inter_warehouse_route_values(self, supplier_warehouse):
|
||||||
return {
|
return {
|
||||||
'name': _('%s: Supply Product from %s') % (self.name, supplier_warehouse.name),
|
'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,
|
'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
|
# Pull / Push tools
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def _get_rule_values(self, route_values, values=None, name_suffix=''):
|
def _get_rule_values(self, route_values, values=None, name_suffix=''):
|
||||||
first_rule = True
|
first_rule = True
|
||||||
rules_list = []
|
rules_list = []
|
||||||
@@ -484,7 +580,7 @@ class Warehouse(models.Model):
|
|||||||
'action': routing.action,
|
'action': routing.action,
|
||||||
'auto': 'manual',
|
'auto': 'manual',
|
||||||
'picking_type_id': routing.picking_type.id,
|
'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,
|
'warehouse_id': self.id,
|
||||||
'company_id': self.company_id.id,
|
'company_id': self.company_id.id,
|
||||||
'propagate': routing.picking_type != self.pick_type_id,
|
'propagate': routing.picking_type != self.pick_type_id,
|
||||||
@@ -494,14 +590,6 @@ class Warehouse(models.Model):
|
|||||||
first_rule = False
|
first_rule = False
|
||||||
return rules_list
|
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):
|
def _get_supply_pull_rules_values(self, route_values, values=None):
|
||||||
pull_values = {}
|
pull_values = {}
|
||||||
pull_values.update(values)
|
pull_values.update(values)
|
||||||
@@ -539,7 +627,7 @@ class Warehouse(models.Model):
|
|||||||
else:
|
else:
|
||||||
# We need to delete all the MTO stock rules, otherwise they risk to be used in the system
|
# We need to delete all the MTO stock rules, otherwise they risk to be used in the system
|
||||||
Rule.search([
|
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'),
|
('location_id.usage', '=', 'transit'),
|
||||||
('action', '!=', 'push'),
|
('action', '!=', 'push'),
|
||||||
('location_src_id', '=', self.lot_stock_id.id)]).write({'active': False})
|
('location_src_id', '=', self.lot_stock_id.id)]).write({'active': False})
|
||||||
@@ -556,29 +644,6 @@ class Warehouse(models.Model):
|
|||||||
('location_src_id.usage', '=', 'transit')
|
('location_src_id.usage', '=', 'transit')
|
||||||
]).write({'location_id': new_location.id})
|
]).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):
|
def _update_name_and_code(self, new_name=False, new_code=False):
|
||||||
if new_code:
|
if new_code:
|
||||||
self.mapped('lot_stock_id').mapped('location_id').write({'name': new_code})
|
self.mapped('lot_stock_id').mapped('location_id').write({'name': new_code})
|
||||||
@@ -635,19 +700,74 @@ class Warehouse(models.Model):
|
|||||||
# Misc
|
# Misc
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
def _get_picking_type_values(self, reception_steps, delivery_steps, pack_stop_location):
|
def _get_picking_type_update_values(self):
|
||||||
input_loc, output_loc = self._get_input_output_locations(reception_steps, delivery_steps)
|
""" 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 {
|
return {
|
||||||
'in_type_id': {'default_location_dest_id': input_loc.id},
|
'in_type_id': {'default_location_dest_id': input_loc.id},
|
||||||
'out_type_id': {'default_location_src_id': output_loc.id},
|
'out_type_id': {'default_location_src_id': output_loc.id},
|
||||||
'pick_type_id': {
|
'pick_type_id': {
|
||||||
'active': delivery_steps != 'ship_only',
|
'active': self.delivery_steps != 'ship_only',
|
||||||
'default_location_dest_id': output_loc.id if delivery_steps == 'pick_ship' else pack_stop_location.id},
|
'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': delivery_steps == 'pick_pack_ship'},
|
'pack_type_id': {'active': self.delivery_steps == 'pick_pack_ship'},
|
||||||
'int_type_id': {},
|
'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):
|
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 {
|
return {
|
||||||
'in_type_id': {
|
'in_type_id': {
|
||||||
'name': self.name + ' ' + _('Sequence in'),
|
'name': self.name + ' ' + _('Sequence in'),
|
||||||
@@ -677,7 +797,9 @@ class Warehouse(models.Model):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _format_rulename(self, from_loc, dest_loc, suffix):
|
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:
|
if suffix:
|
||||||
rulename += ' (' + suffix + ')'
|
rulename += ' (' + suffix + ')'
|
||||||
return rulename
|
return rulename
|
||||||
|
|||||||
@@ -332,7 +332,6 @@ class TestWarehouse(TestStockCommon):
|
|||||||
|
|
||||||
route_shop_namur = warehouse_shop_namur.resupply_route_ids
|
route_shop_namur = warehouse_shop_namur.resupply_route_ids
|
||||||
route_shop_wavre = warehouse_shop_wavre.resupply_route_ids
|
route_shop_wavre = warehouse_shop_wavre.resupply_route_ids
|
||||||
|
|
||||||
# The product contains the 2 resupply routes.
|
# The product contains the 2 resupply routes.
|
||||||
product = self.env['product.product'].create({
|
product = self.env['product.product'].create({
|
||||||
'name': 'Fakir',
|
'name': 'Fakir',
|
||||||
|
|||||||
@@ -35,9 +35,10 @@
|
|||||||
<field name="reception_steps" widget='radio'/>
|
<field name="reception_steps" widget='radio'/>
|
||||||
<field name="delivery_steps" widget='radio'/>
|
<field name="delivery_steps" widget='radio'/>
|
||||||
</group>
|
</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="warehouse_count" invisible="1"/>
|
||||||
<field name="resupply_wh_ids" domain="[('id', '!=', id)]" widget='many2many_checkboxes' attrs="{'invisible': [('warehouse_count', '<', 1)]}"/>
|
<field name="resupply_wh_ids" attrs="{'invisible': [('show_resupply', '!=', True)]}" domain="[('id', '!=', id)]" widget='many2many_checkboxes'/>
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
</page>
|
</page>
|
||||||
|
|||||||
Reference in New Issue
Block a user