odoo/addons/stock/models/stock_warehouse.py

878 lines
48 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import namedtuple
from datetime import datetime
from dateutil import relativedelta
from odoo import api, fields, models, _
from odoo.addons import decimal_precision as dp
from odoo.exceptions import UserError, ValidationError
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
import logging
_logger = logging.getLogger(__name__)
class Warehouse(models.Model):
_name = "stock.warehouse"
_description = "Warehouse"
# namedtuple used in helper methods generating values for routes
Routing = namedtuple('Routing', ['from_loc', 'dest_loc', 'picking_type'])
name = fields.Char('Warehouse Name', index=True, required=True, default=lambda self: self.env['res.company']._company_default_get('stock.inventory').name)
active = fields.Boolean('Active', default=True)
company_id = fields.Many2one(
'res.company', 'Company', default=lambda self: self.env['res.company']._company_default_get('stock.inventory'),
index=True, readonly=True, required=True,
help='The company is automatically set from your user preferences.')
partner_id = fields.Many2one('res.partner', 'Address')
view_location_id = fields.Many2one('stock.location', 'View Location', domain=[('usage', '=', 'view')], required=True)
lot_stock_id = fields.Many2one('stock.location', 'Location Stock', domain=[('usage', '=', 'internal')], required=True)
code = fields.Char('Short Name', required=True, size=5, help="Short name used to identify your warehouse")
route_ids = fields.Many2many(
'stock.location.route', 'stock_route_warehouse', 'warehouse_id', 'route_id',
'Routes', domain="[('warehouse_selectable', '=', True)]",
help='Defaults routes through the warehouse')
reception_steps = fields.Selection([
('one_step', 'Receive goods directly in stock (1 step)'),
('two_steps', 'Unload in input location then go to stock (2 steps)'),
('three_steps', 'Unload in input location, go through a quality control before being admitted in stock (3 steps)')],
'Incoming Shipments', default='one_step', required=True,
help="Default incoming route to follow")
delivery_steps = fields.Selection([
('ship_only', 'Ship directly from stock (Ship only)'),
('pick_ship', 'Bring goods to output location before shipping (Pick + Ship)'),
('pick_pack_ship', 'Make packages into a dedicated location, then bring them to the output location for shipping (Pick + Pack + Ship)')],
'Outgoing Shippings', default='ship_only', required=True,
help="Default outgoing route to follow")
wh_input_stock_loc_id = fields.Many2one('stock.location', 'Input Location')
wh_qc_stock_loc_id = fields.Many2one('stock.location', 'Quality Control Location')
wh_output_stock_loc_id = fields.Many2one('stock.location', 'Output Location')
wh_pack_stock_loc_id = fields.Many2one('stock.location', 'Packing Location')
mto_pull_id = fields.Many2one('procurement.rule', 'MTO rule')
pick_type_id = fields.Many2one('stock.picking.type', 'Pick Type')
pack_type_id = fields.Many2one('stock.picking.type', 'Pack Type')
out_type_id = fields.Many2one('stock.picking.type', 'Out Type')
in_type_id = fields.Many2one('stock.picking.type', 'In Type')
int_type_id = fields.Many2one('stock.picking.type', 'Internal Type')
crossdock_route_id = fields.Many2one('stock.location.route', 'Crossdock Route')
reception_route_id = fields.Many2one('stock.location.route', 'Receipt Route')
delivery_route_id = fields.Many2one('stock.location.route', 'Delivery Route')
resupply_wh_ids = fields.Many2many(
'stock.warehouse', 'stock_wh_resupply_table', 'supplied_wh_id', 'supplier_wh_id',
'Resupply Warehouses')
resupply_route_ids = fields.One2many(
'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")
default_resupply_wh_id = fields.Many2one(
'stock.warehouse', 'Default Resupply Warehouse',
help="Goods will always be resupplied from this warehouse")
_sql_constraints = [
('warehouse_name_uniq', 'unique(name, company_id)', 'The name of the warehouse must be unique per company!'),
('warehouse_code_uniq', 'unique(code, company_id)', 'The code of the warehouse must be unique per company!'),
]
@api.depends('default_resupply_wh_id', 'resupply_wh_ids')
def onchange_resupply_warehouses(self):
# If we are removing the default resupply, we don't have default_resupply_wh_id # TDE note: and we want one
self.resupply_wh_ids |= self.default_resupply_wh_id
@api.model
def create(self, vals):
# create view location for warehouse then create all locations
loc_vals = {'name': _(vals.get('code')), 'usage': 'view',
'location_id': self.env.ref('stock.stock_location_locations').id}
if vals.get('company_id'):
loc_vals['company_id'] = vals.get('company_id')
vals['view_location_id'] = self.env['stock.location'].create(loc_vals).id
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.iteritems():
values['location_id'] = vals['view_location_id']
if vals.get('company_id'):
values['company_id'] = vals.get('company_id')
vals[field_name] = self.env['stock.location'].with_context(active_test=False).create(values).id
# actually create WH
warehouse = super(Warehouse, self).create(vals)
# create sequences and picking types
new_vals = warehouse.create_sequences_and_picking_types()
warehouse.write(new_vals) # TDE FIXME: use super ?
# create routes and push/procurement rules
route_vals = warehouse.create_routes()
warehouse.write(route_vals)
# update partner data if partner assigned
if vals.get('partner_id'):
self._update_partner_data(vals['partner_id'], vals.get('company_id'))
return warehouse
@api.multi
def write(self, vals):
Route = self.env['stock.location.route']
warehouses = self.with_context(active_test=False) # TDE FIXME: check this
if vals.get('code') or vals.get('name'):
warehouses._update_name_and_code(vals.get('name'), vals.get('code'))
# activate and deactivate location according to reception and delivery option
if vals.get('reception_steps'):
warehouses._update_location_reception(vals['reception_steps'])
if vals.get('delivery_steps'):
warehouses._update_location_delivery(vals['delivery_steps'])
if vals.get('reception_steps') or vals.get('delivery_steps'):
warehouses._update_reception_delivery_resupply(vals.get('reception_steps'), vals.get('delivery_steps'))
if vals.get('resupply_wh_ids') and not vals.get('resupply_route_ids'):
resupply_whs = self.resolve_2many_commands('resupply_wh_ids', vals['resupply_wh_ids'])
new_resupply_whs = self.browse([wh['id'] for wh in resupply_whs])
old_resupply_whs = {warehouse.id: warehouse.resupply_wh_ids for warehouse in warehouses}
if 'default_resupply_wh_id' in vals:
if vals.get('default_resupply_wh_id') and any(vals['default_resupply_wh_id'] == warehouse.id for warehouse in warehouses):
raise UserError(_('The default resupply warehouse should be different than the warehouse itself!'))
for warehouse in warehouses.filtered(lambda wh: wh.default_resupply_wh_id):
# remove the existing resupplying route on the warehouse
to_remove_routes = Route.search([('supplied_wh_id', '=', warehouse.id), ('supplier_wh_id', '=', warehouse.default_resupply_wh_id.id)])
for inter_wh_route in to_remove_routes:
warehouse.write({'route_ids': [(3, inter_wh_route.id)]})
# If another partner assigned
if vals.get('partner_id'):
warehouses._update_partner_data(vals['partner_id'], vals.get('company_id'))
res = super(Warehouse, self).write(vals)
# check if we need to delete and recreate route
if vals.get('reception_steps') or vals.get('delivery_steps'):
route_vals = warehouses._update_routes()
if route_vals:
self.write(route_vals)
if vals.get('resupply_wh_ids') and not vals.get('resupply_route_ids'):
for warehouse in warehouses:
to_add = new_resupply_whs - old_resupply_whs[warehouse.id]
to_remove = old_resupply_whs[warehouse.id] - new_resupply_whs
if to_add:
warehouse.create_resupply_routes(to_add, warehouse.default_resupply_wh_id)
if to_remove:
Route.search([('supplied_wh_id', '=', warehouse.id), ('supplier_wh_id', 'in', to_remove.ids)]).unlink()
# TDE FIXME: shouldn't we remove procurement rules also ? because this could make them global (not sure)
return res
@api.model
def _update_partner_data(self, partner_id, company_id):
if not partner_id:
return
ResCompany = self.env['res.company']
if company_id:
transit_loc = ResCompany.browse(company_id).internal_transit_location_id.id
else:
transit_loc = ResCompany._company_default_get('stock.warehouse').internal_transit_location_id.id
self.env['res.partner'].browse(partner_id).write({'property_stock_customer': transit_loc, 'property_stock_supplier': transit_loc})
def create_sequences_and_picking_types(self):
IrSequenceSudo = self.env['ir.sequence'].sudo()
PickingType = self.env['stock.picking.type']
input_loc, output_loc = self._get_input_output_locations(self.reception_steps, self.delivery_steps)
# choose the next available color for the picking types of this warehouse
all_used_colors = [res['color'] for res in PickingType.search_read([('warehouse_id', '!=', False), ('color', '!=', False)], ['color'], order='color')]
available_colors = [zef for zef in [0, 3, 4, 5, 6, 7, 8, 1, 2] if zef not in all_used_colors]
color = available_colors and available_colors[0] or 0
# suit for each warehouse: reception, internal, pick, pack, ship
max_sequence = PickingType.search_read([('sequence', '!=', False)], ['sequence'], limit=1, order='sequence desc')
max_sequence = max_sequence and max_sequence[0]['sequence'] or 0
warehouse_data = {}
sequence_data = self._get_sequence_values()
# tde todo: backport sequence fix
create_data = {
'in_type_id': {
'name': _('Receipts'),
'code': 'incoming',
'use_create_lots': True,
'use_existing_lots': False,
'default_location_src_id': False,
'sequence': max_sequence + 1,
}, 'out_type_id': {
'name': _('Delivery Orders'),
'code': 'outgoing',
'use_create_lots': False,
'use_existing_lots': True,
'default_location_dest_id': False,
'sequence': max_sequence + 5,
}, 'pack_type_id': {
'name': _('Pack'),
'code': 'internal',
'use_create_lots': False,
'use_existing_lots': True,
'default_location_src_id': self.wh_pack_stock_loc_id.id,
'default_location_dest_id': output_loc.id,
'sequence': max_sequence + 4,
}, 'pick_type_id': {
'name': _('Pick'),
'code': 'internal',
'use_create_lots': False,
'use_existing_lots': True,
'default_location_src_id': self.lot_stock_id.id,
'sequence': max_sequence + 3,
}, 'int_type_id': {
'name': _('Internal Transfers'),
'code': 'internal',
'use_create_lots': False,
'use_existing_lots': True,
'default_location_src_id': self.lot_stock_id.id,
'default_location_dest_id': self.lot_stock_id.id,
'active': self.reception_steps != 'one_step' or self.delivery_steps != 'ship_only' or self.user_has_groups('stock.group_stock_multi_locations'),
'sequence': max_sequence + 2,
},
}
data = self._get_picking_type_values(self.reception_steps, self.delivery_steps, self.wh_pack_stock_loc_id)
for field_name, values in data.iteritems():
data[field_name].update(create_data[field_name])
for picking_type, values in data.iteritems():
sequence = IrSequenceSudo.create(sequence_data[picking_type])
values.update(warehouse_id=self.id, color=color, sequence_id=sequence.id)
warehouse_data[picking_type] = PickingType.create(values).id
PickingType.browse(warehouse_data['out_type_id']).write({'return_picking_type_id': warehouse_data['in_type_id']})
PickingType.browse(warehouse_data['in_type_id']).write({'return_picking_type_id': warehouse_data['out_type_id']})
return warehouse_data
@api.multi
def create_routes(self):
self.ensure_one()
routes_data = self.get_routes_dict()
reception_route = self._create_or_update_reception_route(routes_data)
delivery_route = self._create_or_update_delivery_route(routes_data)
mto_pull = self._create_or_update_mto_pull(routes_data)
crossdock_route = self._create_or_update_crossdock_route(routes_data)
# create route selectable on the product to resupply the warehouse from another one
self.create_resupply_routes(self.resupply_wh_ids, self.default_resupply_wh_id)
# return routes and mto procurement rule to store on the warehouse
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,
}
def _create_or_update_reception_route(self, routes_data):
routes_data = routes_data or self.get_routes_dict()
for warehouse in self:
if warehouse.reception_route_id:
reception_route = warehouse.reception_route_id
reception_route.write({'name': warehouse._format_routename(route_type=warehouse.reception_steps)})
reception_route.pull_ids.unlink()
reception_route.push_ids.unlink()
else:
warehouse.reception_route_id = reception_route = self.env['stock.location.route'].create(warehouse._get_reception_delivery_route_values(warehouse.reception_steps))
# push / procurement (pull) rules for reception
routings = routes_data[warehouse.id][warehouse.reception_steps]
push_rules_list, pull_rules_list = warehouse._get_push_pull_rules_values(
routings, values={'active': True, 'route_id': reception_route.id},
push_values=None, pull_values={'procure_method': 'make_to_order'})
for push_vals in push_rules_list:
self.env['stock.location.path'].create(push_vals)
for pull_vals in pull_rules_list:
self.env['procurement.rule'].create(pull_vals)
return reception_route
def _create_or_update_delivery_route(self, routes_data):
""" Delivery (MTS) route """
routes_data = routes_data or self.get_routes_dict()
for warehouse in self:
if warehouse.delivery_route_id:
delivery_route = warehouse.delivery_route_id
delivery_route.write({'name': warehouse._format_routename(route_type=warehouse.delivery_steps)})
delivery_route.pull_ids.unlink()
else:
delivery_route = self.env['stock.location.route'].create(warehouse._get_reception_delivery_route_values(warehouse.delivery_steps))
# procurement (pull) rules for delivery
routings = routes_data[warehouse.id][warehouse.delivery_steps]
dummy, pull_rules_list = warehouse._get_push_pull_rules_values(
routings, values={'active': True, 'route_id': delivery_route.id})
for pull_vals in pull_rules_list:
self.env['procurement.rule'].create(pull_vals)
return delivery_route
def _create_or_update_mto_pull(self, routes_data):
""" Create MTO procurement rule and link it to the generic MTO route """
routes_data = routes_data or self.get_routes_dict()
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_pull_rules_values(routings)[0])
else:
mto_pull = self.env['procurement.rule'].create(warehouse._get_mto_pull_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']
dummy, pull_rules_list = warehouse._get_push_pull_rules_values(
routings,
values={'active': warehouse.delivery_steps != 'ship_only' and warehouse.reception_steps != 'one_step', 'route_id': crossdock_route.id},
push_values=None, pull_values={'procure_method': 'make_to_order'})
for pull_vals in pull_rules_list:
self.env['procurement.rule'].create(pull_vals)
return crossdock_route
def create_resupply_routes(self, supplier_warehouses, default_resupply_wh):
Route = self.env['stock.location.route']
Pull = self.env['procurement.rule']
input_location, output_location = self._get_input_output_locations(self.reception_steps, self.delivery_steps)
internal_transit_location, external_transit_location = self._get_transit_locations()
for supplier_wh in supplier_warehouses:
transit_location = internal_transit_location if supplier_wh.company_id == self.company_id else external_transit_location
if not transit_location:
continue
output_location = supplier_wh.lot_stock_id if supplier_wh.delivery_steps == 'ship_only' else supplier_wh.wh_output_stock_loc_id
# Create extra MTO rule (only for 'ship only' because in the other cases MTO rules already exists)
if supplier_wh.delivery_steps == 'ship_only':
Pull.create(supplier_wh._get_mto_pull_rules_values([
self.Routing(output_location, transit_location, supplier_wh.out_type_id)])[0])
inter_wh_route = Route.create(self._get_inter_warehouse_route_values(supplier_wh))
pull_rules_list = supplier_wh._get_supply_pull_rules_values(
[self.Routing(output_location, transit_location, supplier_wh.out_type_id)],
values={'route_id': inter_wh_route.id, 'propagate_warehouse_id': self.id})
pull_rules_list += self._get_supply_pull_rules_values(
[self.Routing(transit_location, input_location, self.in_type_id)],
values={'route_id': inter_wh_route.id, 'propagate_warehouse_id': supplier_wh.id})
for pull_rule_vals in pull_rules_list:
Pull.create(pull_rule_vals)
# if the warehouse is also set as default resupply method, assign this route automatically to the warehouse
if supplier_wh == default_resupply_wh:
(self | supplier_wh).write({'route_ids': [(4, inter_wh_route.id)]})
# Routing tools
# ------------------------------------------------------------
def _get_input_output_locations(self, reception_steps, delivery_steps):
return (self.lot_stock_id if reception_steps == 'one_step' else self.wh_input_stock_loc_id,
self.lot_stock_id if delivery_steps == 'ship_only' else self.wh_output_stock_loc_id)
def _get_transit_locations(self):
return self.company_id.internal_transit_location_id, self.env.ref('stock.stock_location_inter_wh', raise_if_not_found=False) or self.env['stock.location']
@api.model
def _get_partner_locations(self):
''' returns a tuple made of the browse record of customer location and the browse record of supplier location'''
Location = self.env['stock.location']
customer_loc = self.env.ref('stock.stock_location_customers', raise_if_not_found=False)
supplier_loc = self.env.ref('stock.stock_location_suppliers', raise_if_not_found=False)
if not customer_loc:
customer_loc = Location.search([('usage', '=', 'customer')], limit=1)
if not supplier_loc:
supplier_loc = Location.search([('usage', '=', 'supplier')], limit=1)
if not customer_loc and not supplier_loc:
raise UserError(_('Can\'t find any customer or supplier location.'))
return customer_loc, supplier_loc
def _get_route_name(self, route_type):
names = {'one_step': _('Receipt in 1 step'), 'two_steps': _('Receipt in 2 steps'),
'three_steps': _('Receipt in 3 steps'), 'crossdock': _('Cross-Dock'),
'ship_only': _('Ship Only'), 'pick_ship': _('Pick + Ship'),
'pick_pack_ship': _('Pick + Pack + Ship')}
return names[route_type]
def get_routes_dict(self):
# TDE todo: rename me (was get_routes_dict)
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)],
'three_steps': [
self.Routing(warehouse.wh_input_stock_loc_id, warehouse.wh_qc_stock_loc_id, warehouse.int_type_id),
self.Routing(warehouse.wh_qc_stock_loc_id, warehouse.lot_stock_id, warehouse.int_type_id)],
'crossdock': [
self.Routing(warehouse.wh_input_stock_loc_id, warehouse.wh_output_stock_loc_id, warehouse.int_type_id),
self.Routing(warehouse.wh_output_stock_loc_id, customer_loc, warehouse.out_type_id)],
'ship_only': [self.Routing(warehouse.lot_stock_id, customer_loc, warehouse.out_type_id)],
'pick_ship': [
self.Routing(warehouse.lot_stock_id, warehouse.wh_output_stock_loc_id, warehouse.pick_type_id),
self.Routing(warehouse.wh_output_stock_loc_id, customer_loc, warehouse.out_type_id)],
'pick_pack_ship': [
self.Routing(warehouse.lot_stock_id, warehouse.wh_pack_stock_loc_id, warehouse.pick_type_id),
self.Routing(warehouse.wh_pack_stock_loc_id, warehouse.wh_output_stock_loc_id, warehouse.pack_type_id),
self.Routing(warehouse.wh_output_stock_loc_id, customer_loc, warehouse.out_type_id)],
'company_id': warehouse.company_id.id,
}) for warehouse in self)
@api.multi
def _get_reception_delivery_route_values(self, route_type):
return {
'name': self._format_routename(route_type=route_type),
'product_categ_selectable': True,
'product_selectable': False,
'sequence': 10,
'company_id': self.company_id.id,
}
@api.model
@api.returns('stock.location.route', lambda value: value.id)
def _get_mto_route(self):
mto_route = self.env.ref('stock.route_warehouse0_mto', raise_if_not_found=False)
if not mto_route:
mto_route = self.env['stock.location.route'].search([('name', 'like', _('Make To Order'))], limit=1)
if not mto_route:
raise UserError(_('Can\'t find any generic Make To Order route.'))
return mto_route
def _get_inter_warehouse_route_values(self, supplier_warehouse):
return {
'name': _('%s: Supply Product from %s') % (self.name, supplier_warehouse.name),
'warehouse_selectable': False,
'product_selectable': True,
'product_categ_selectable': True,
'supplied_wh_id': self.id,
'supplier_wh_id': supplier_warehouse.id,
'company_id': self.company_id.id}
def _get_inter_wh_route(self, supplier_warehouse):
# FIXME - remove me in master/saas-14
_logger.warning("'_get_inter_wh_route' has been renamed into '_get_inter_warehouse_route_values'... Overrides are ignored")
return self._get_inter_warehouse_route_values(supplier_warehouse)
def _get_crossdock_route_values(self):
return {
'name': self._format_routename(route_type='crossdock'),
'warehouse_selectable': False,
'product_selectable': True,
'product_categ_selectable': True,
'active': self.delivery_steps != 'ship_only' and self.reception_steps != 'one_step',
'sequence': 20,
'company_id': self.company_id.id}
def _get_crossdock_route(self, route_name):
# FIXME - remove me in master/saas-14
_logger.warning("'_get_crossdock_route' has been renamed into '_get_crossdock_route_values'... Overrides are ignored")
return self._get_crossdock_route_values(route_name)
# Pull / Push tools
# ------------------------------------------------------------
@api.multi
def _get_push_pull_rules_values(self, route_values, values=None, push_values=None, pull_values=None, name_suffix=''):
first_rule = True
push_rules_list, pull_rules_list = [], []
for routing in route_values:
route_push_values = {
'name': self._format_rulename(routing.from_loc, routing.dest_loc, name_suffix),
'location_from_id': routing.from_loc.id,
'location_dest_id': routing.dest_loc.id,
'auto': 'manual',
'picking_type_id': routing.picking_type.id,
'warehouse_id': self.id}
route_push_values.update((values or {}).items() + (push_values or {}).items())
push_rules_list.append(route_push_values)
route_pull_values = {
'name': self._format_rulename(routing.from_loc, routing.dest_loc, name_suffix),
'location_src_id': routing.from_loc.id,
'location_id': routing.dest_loc.id,
'action': 'move',
'picking_type_id': routing.picking_type.id,
'procure_method': first_rule is True and 'make_to_stock' or 'make_to_order',
'warehouse_id': self.id}
route_pull_values.update((values or {}).items() + (pull_values or {}).items())
pull_rules_list.append(route_pull_values)
first_rule = False
return push_rules_list, pull_rules_list
def _get_mto_pull_rules_values(self, route_values):
mto_route = self._get_mto_route()
dummy, pull_rules_list = self._get_push_pull_rules_values(route_values, pull_values={
'route_id': mto_route.id,
'procure_method': 'make_to_order',
'active': True}, name_suffix=_('MTO'))
return pull_rules_list
def _get_mto_pull_rule(self, route_values):
# FIXME - remove me in master/saas-14
_logger.warning("'_get_mto_pull_rule' has been renamed into '_get_mto_pull_rules_values'... Overrides are ignored")
return self._get_mto_pull_rules_values(route_values)
def _get_push_pull_rules(self, active, values, new_route_id):
# FIXME - remove me in master/saas-14
_logger.warning("'_get_push_pull_rules' has been renamed into '_get_push_pull_rules_values'... Overrides are ignored")
return self._get_push_pull_rules_values(values, values={'active': active, 'route_id': new_route_id})
def _get_supply_pull_rules_values(self, route_values, values=None):
dummy, pull_rules_list = self._get_push_pull_rules_values(route_values, values=values, pull_values={'active': True})
for pull_rules in pull_rules_list:
pull_rules['procure_method'] = self.lot_stock_id.id != pull_rules['location_src_id'] and 'make_to_order' or 'make_to_stock' # first part of the resuply route is MTS
return pull_rules_list
def _update_reception_delivery_resupply(self, reception_new, delivery_new):
""" Check if we need to change something to resupply warehouses and associated MTO rules """
input_loc, output_loc = self._get_input_output_locations(reception_new, delivery_new)
for warehouse in self:
if reception_new and warehouse.reception_steps != reception_new and (warehouse.reception_steps == 'one_step' or reception_new == 'one_step'):
warehouse._check_reception_resupply(input_loc)
if delivery_new and warehouse.delivery_steps != delivery_new and (warehouse.delivery_steps == 'ship_only' or delivery_new == 'ship_only'):
change_to_multiple = warehouse.delivery_steps == 'ship_only'
warehouse._check_delivery_resupply(output_loc, change_to_multiple)
def _check_resupply(self, reception_new, delivery_new):
# FIXME - remove me in master/saas-14
_logger.warning("'_check_resupply' has been renamed into '_update_reception_delivery_resupply'... Overrides are ignored")
return self._update_reception_delivery_resupply(reception_new, delivery_new)
def _check_delivery_resupply(self, new_location, change_to_multiple):
""" Check if the resupply routes from this warehouse follow the changes of number of delivery steps
Check routes being delivery bu this warehouse and change the rule going to transit location """
Pull = self.env["procurement.rule"]
routes = self.env['stock.location.route'].search([('supplier_wh_id', '=', self.id)])
pulls = Pull.search(['&', ('route_id', 'in', routes.ids), ('location_id.usage', '=', 'transit')])
pulls.write({
'location_src_id': new_location.id,
'procure_method': change_to_multiple and "make_to_order" or "make_to_stock"})
if not change_to_multiple:
# If single delivery we should create the necessary MTO rules for the resupply
routings = [self.Routing(self.lot_stock_id , location, self.out_type_id) for location in pulls.mapped('location_id')]
mto_pull_vals = self._get_mto_pull_rules_values(routings)
for mto_pull_val in mto_pull_vals:
Pull.create(mto_pull_val)
else:
# We need to delete all the MTO procurement rules, otherwise they risk to be used in the system
Pull.search([
'&', ('route_id', '=', self._get_mto_route().id),
('location_id.usage', '=', 'transit'),
('location_src_id', '=', self.lot_stock_id.id)]).unlink()
def _check_reception_resupply(self, new_location):
""" Check routes being delivered by the warehouses (resupply routes) and
change their rule coming from the transit location """
routes = self.env['stock.location.route'].search([('supplied_wh_id', 'in', self.ids)])
self.env['procurement.rule'].search([
'&', ('route_id', 'in', routes.ids),
('location_src_id.usage', '=', 'transit')]).write({'location_id': new_location.id})
@api.multi
def _update_routes(self):
routes_data = self.get_routes_dict()
# change the default source and destination location and (de)activate picking types
self._update_picking_type()
# update delivery route and rules: unlink the existing rules of the warehouse delivery route and recreate it
delivery_route = self._create_or_update_delivery_route(routes_data)
# update receipt route and rules: unlink the existing rules of the warehouse receipt route and recreate it
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.multi
def change_route(self):
# FIXME - remove me in master/saas-14
_logger.warning("'change_route' has been renamed into '_update_routes'... Overrides are ignored")
return self._update_routes()
@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.iteritems():
getattr(self, field_name).write(values)
@api.multi
def _update_name_and_code(self, new_name=False, new_code=False):
if new_code:
self.mapped('lot_stock_id').mapped('location_id').write({'name': new_code})
if new_name:
# TDE FIXME: replacing the route name ? not better to re-generate the route naming ?
for warehouse in self:
routes = warehouse.route_ids
for route in routes:
route.write({'name': route.name.replace(warehouse.name, new_name, 1)})
for pull in route.pull_ids:
pull.write({'name': pull.name.replace(warehouse.name, new_name, 1)})
for push in route.push_ids:
push.write({'name': push.name.replace(warehouse.name, new_name, 1)})
if warehouse.mto_pull_id:
warehouse.mto_pull_id.write({'name': warehouse.mto_pull_id.name.replace(warehouse.name, new_name, 1)})
for warehouse in self:
sequence_data = warehouse._get_sequence_values()
warehouse.in_type_id.sequence_id.write(sequence_data['in_type_id'])
warehouse.out_type_id.sequence_id.write(sequence_data['out_type_id'])
warehouse.pack_type_id.sequence_id.write(sequence_data['pack_type_id'])
warehouse.pick_type_id.sequence_id.write(sequence_data['pick_type_id'])
warehouse.int_type_id.sequence_id.write(sequence_data['int_type_id'])
@api.multi
def _handle_renaming(self, new_name=False, new_code=False):
# FIXME - remove me in master/saas-14
_logger.warning("'_handle_renaming' has been renamed into '_update_name_and_code'... Overrides are ignored")
return self._update_name_and_code(new_name=new_name, new_code=new_code)
def _update_location_reception(self, new_reception_step):
switch_warehouses = self.filtered(lambda wh: wh.reception_steps != new_reception_step and not wh._location_used(wh.wh_input_stock_loc_id))
if switch_warehouses:
(switch_warehouses.mapped('wh_input_stock_loc_id') + switch_warehouses.mapped('wh_qc_stock_loc_id')).write({'active': False})
if new_reception_step == 'three_steps':
self.mapped('wh_qc_stock_loc_id').write({'active': True})
if new_reception_step != 'one_step':
self.mapped('wh_input_stock_loc_id').write({'active': True})
def _update_location_delivery(self, new_delivery_step):
switch_warehouses = self.filtered(lambda wh: wh.delivery_steps != new_delivery_step)
loc_warehouse = switch_warehouses.filtered(lambda wh: not wh._location_used(wh.wh_output_stock_loc_id))
if loc_warehouse:
loc_warehouse.mapped('wh_output_stock_loc_id').write({'active': False})
loc_warehouse = switch_warehouses.filtered(lambda wh: not wh._location_used(wh.wh_pack_stock_loc_id))
if loc_warehouse:
loc_warehouse.mapped('wh_pack_stock_loc_id').write({'active': False})
if new_delivery_step == 'pick_pack_ship':
self.mapped('wh_pack_stock_loc_id').write({'active': True})
if new_delivery_step != 'ship_only':
self.mapped('wh_output_stock_loc_id').write({'active': True})
def _location_used(self, location):
pulls = self.env['procurement.rule'].search_count([
'&',
('route_id', 'not in', [x.id for x in self.route_ids]),
'|', ('location_src_id', '=', location.id),
('location_id', '=', location.id)])
if pulls:
return True
pushs = self.env['stock.location.path'].search_count([
'&',
('route_id', 'not in', [x.id for x in self.route_ids]),
'|', ('location_from_id', '=', location.id),
('location_dest_id', '=', location.id)])
if pushs:
return True
return False
# Misc
# ------------------------------------------------------------
def _get_picking_type_values(self, reception_steps, delivery_steps, pack_stop_location):
input_loc, output_loc = self._get_input_output_locations(reception_steps, delivery_steps)
return {
'in_type_id': {'default_location_dest_id': input_loc.id},
'out_type_id': {'default_location_src_id': output_loc.id},
'pick_type_id': {
'active': delivery_steps != 'ship_only',
'default_location_dest_id': output_loc.id if delivery_steps == 'pick_ship' else pack_stop_location.id},
'pack_type_id': {'active': delivery_steps == 'pick_pack_ship'},
'int_type_id': {},
}
def _get_sequence_values(self):
return {
'in_type_id': {'name': self.name + ' ' + _('Sequence in'), 'prefix': self.code + '/IN/', 'padding': 5},
'out_type_id': {'name': self.name + ' ' + _('Sequence out'), 'prefix': self.code + '/OUT/', 'padding': 5},
'pack_type_id': {'name': self.name + ' ' + _('Sequence packing'), 'prefix': self.code + '/PACK/', 'padding': 5},
'pick_type_id': {'name': self.name + ' ' + _('Sequence picking'), 'prefix': self.code + '/PICK/', 'padding': 5},
'int_type_id': {'name': self.name + ' ' + _('Sequence internal'), 'prefix': self.code + '/INT/', 'padding': 5},
}
@api.multi
def _format_rulename(self, from_loc, dest_loc, suffix):
return '%s: %s -> %s%s' % (self.code, from_loc.name, dest_loc.name, suffix)
@api.multi
def _format_routename(self, name=None, route_type=None):
if route_type:
name = self._get_route_name(route_type)
return '%s: %s' % (self.name, name)
@api.returns('self')
@api.multi
def _get_all_routes(self):
# TDE FIXME: check overrides
routes = self.mapped('route_ids') | self.mapped('mto_pull_id').mapped('route_id')
routes |= self.env["stock.location.route"].search([('supplied_wh_id', 'in', self.ids)])
return routes
get_all_routes_for_wh = _get_all_routes
@api.multi
def action_view_all_routes(self):
routes = self._get_all_routes()
return {
'name': _('Warehouse\'s Routes'),
'domain': [('id', 'in', routes.ids)],
'res_model': 'stock.location.route',
'type': 'ir.actions.act_window',
'view_id': False,
'view_mode': 'tree,form',
'view_type': 'form',
'limit': 20
}
class Orderpoint(models.Model):
""" Defines Minimum stock rules. """
_name = "stock.warehouse.orderpoint"
_description = "Minimum Inventory Rule"
@api.model
def default_get(self, fields):
res = super(Orderpoint, self).default_get(fields)
warehouse = None
if 'warehouse_id' not in res and res.get('company_id'):
warehouse = self.env['stock.warehouse'].search([('company_id', '=', res['company_id'])], limit=1)
if warehouse:
res['warehouse_id'] = warehouse.id
res['location_id'] = warehouse.lot_stock_id.id
return res
name = fields.Char(
'Name', copy=False, required=True,
default=lambda self: self.env['ir.sequence'].next_by_code('stock.orderpoint'))
active = fields.Boolean(
'Active', default=True,
help="If the active field is set to False, it will allow you to hide the orderpoint without removing it.")
warehouse_id = fields.Many2one(
'stock.warehouse', 'Warehouse',
ondelete="cascade", required=True)
location_id = fields.Many2one(
'stock.location', 'Location',
ondelete="cascade", required=True)
product_id = fields.Many2one(
'product.product', 'Product',
domain=[('type', '=', 'product')], ondelete='cascade', required=True)
product_uom = fields.Many2one(
'product.uom', 'Product Unit of Measure', related='product_id.uom_id',
readonly=True, required=True,
default=lambda self: self._context.get('product_uom', False))
product_min_qty = fields.Float(
'Minimum Quantity', digits=dp.get_precision('Product Unit of Measure'), required=True,
help="When the virtual stock goes below the Min Quantity specified for this field, Odoo generates "
"a procurement to bring the forecasted quantity to the Max Quantity.")
product_max_qty = fields.Float(
'Maximum Quantity', digits=dp.get_precision('Product Unit of Measure'), required=True,
help="When the virtual stock goes below the Min Quantity, Odoo generates "
"a procurement to bring the forecasted quantity to the Quantity specified as Max Quantity.")
qty_multiple = fields.Float(
'Qty Multiple', digits=dp.get_precision('Product Unit of Measure'),
default=1, required=True,
help="The procurement quantity will be rounded up to this multiple. If it is 0, the exact quantity will be used.")
procurement_ids = fields.One2many('procurement.order', 'orderpoint_id', 'Created Procurements')
group_id = fields.Many2one(
'procurement.group', 'Procurement Group', copy=False,
help="Moves created through this orderpoint will be put in this procurement group. If none is given, the moves generated by procurement rules will be grouped into one big picking.")
company_id = fields.Many2one(
'res.company', 'Company', required=True,
default=lambda self: self.env['res.company']._company_default_get('stock.warehouse.orderpoint'))
lead_days = fields.Integer(
'Lead Time', default=1,
help="Number of days after the orderpoint is triggered to receive the products or to order to the vendor")
lead_type = fields.Selection(
[('net', 'Day(s) to get the products'), ('supplier', 'Day(s) to purchase')], 'Lead Type',
required=True, default='supplier')
_sql_constraints = [
('qty_multiple_check', 'CHECK( qty_multiple >= 0 )', 'Qty Multiple must be greater than or equal to zero.'),
]
@api.constrains('product_id')
def _check_product_uom(self):
''' Check if the UoM has the same category as the product standard UoM '''
if any(orderpoint.product_id.uom_id.category_id != orderpoint.product_uom.category_id for orderpoint in self):
raise ValidationError(_('You have to select a product unit of measure in the same category than the default unit of measure of the product'))
@api.onchange('warehouse_id')
def onchange_warehouse_id(self):
""" Finds location id for changed warehouse. """
if self.warehouse_id:
self.location_id = self.warehouse_id.lot_stock_id.id
@api.onchange('product_id')
def onchange_product_id(self):
if self.product_id:
self.product_uom = self.product_id.uom_id.id
return {'domain': {'product_uom': [('category_id', '=', self.product_id.uom_id.category_id.id)]}}
return {'domain': {'product_uom': []}}
@api.multi
def subtract_procurements_from_orderpoints(self):
'''This function returns quantity of product that needs to be deducted from the orderpoint computed quantity because there's already a procurement created with aim to fulfill it.
'''
self._cr.execute("""SELECT orderpoint.id, procurement.id, procurement.product_uom, procurement.product_qty, template.uom_id, move.product_qty
FROM stock_warehouse_orderpoint orderpoint
JOIN procurement_order AS procurement ON procurement.orderpoint_id = orderpoint.id
JOIN product_product AS product ON product.id = procurement.product_id
JOIN product_template AS template ON template.id = product.product_tmpl_id
LEFT JOIN stock_move AS move ON move.procurement_id = procurement.id
WHERE procurement.state not in ('done', 'cancel')
AND (move.state IS NULL OR move.state != 'draft')
AND orderpoint.id IN %s
ORDER BY orderpoint.id, procurement.id
""", (tuple(self.ids),))
UoM = self.env["product.uom"]
procurements_done = set()
res = dict.fromkeys(self.ids, 0.0)
for orderpoint_id, procurement_id, product_uom_id, procurement_qty, template_uom_id, move_qty in self._cr.fetchall():
if procurement_id not in procurements_done: # count procurement once, if multiple move in this orderpoint/procurement combo
procurements_done.add(procurement_id)
res[orderpoint_id] += UoM.browse(product_uom_id)._compute_quantity(procurement_qty, UoM.browse(template_uom_id), round=False)
if move_qty:
res[orderpoint_id] -= move_qty
return res
def _get_date_planned(self, start_date):
days = self.lead_days or 0.0
if self.lead_type == 'supplier':
# These days will be substracted when creating the PO
qty = self.env.context.get('product_qty', 0.0)
days += self.product_id._select_seller(quantity=qty).delay or 0.0
date_planned = start_date + relativedelta.relativedelta(days=days)
return date_planned.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
@api.multi
def _prepare_procurement_values(self, product_qty, date=False, group=False):
return {
'name': self.name,
'date_planned': date or self.with_context(product_qty=product_qty)._get_date_planned(datetime.today()),
'product_id': self.product_id.id,
'product_qty': product_qty,
'company_id': self.company_id.id,
'product_uom': self.product_uom.id,
'location_id': self.location_id.id,
'origin': self.name,
'warehouse_id': self.warehouse_id.id,
'orderpoint_id': self.id,
'group_id': group or self.group_id.id,
}