1058 lines
54 KiB
Python
1058 lines
54 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from collections import namedtuple
|
|
import json
|
|
import time
|
|
|
|
from odoo import api, fields, models, _
|
|
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
|
|
from odoo.tools.float_utils import float_compare
|
|
from odoo.addons.procurement.models import procurement
|
|
from odoo.exceptions import UserError
|
|
|
|
|
|
class PickingType(models.Model):
|
|
_name = "stock.picking.type"
|
|
_description = "The picking type determines the picking view"
|
|
_order = 'sequence, id'
|
|
|
|
name = fields.Char('Picking Type Name', required=True, translate=True)
|
|
color = fields.Integer('Color')
|
|
sequence = fields.Integer('Sequence', help="Used to order the 'All Operations' kanban view")
|
|
sequence_id = fields.Many2one('ir.sequence', 'Reference Sequence', required=True)
|
|
default_location_src_id = fields.Many2one(
|
|
'stock.location', 'Default Source Location',
|
|
help="This is the default source location when you create a picking manually with this picking type. It is possible however to change it or that the routes put another location. If it is empty, it will check for the supplier location on the partner. ")
|
|
default_location_dest_id = fields.Many2one(
|
|
'stock.location', 'Default Destination Location',
|
|
help="This is the default destination location when you create a picking manually with this picking type. It is possible however to change it or that the routes put another location. If it is empty, it will check for the customer location on the partner. ")
|
|
code = fields.Selection([('incoming', 'Vendors'), ('outgoing', 'Customers'), ('internal', 'Internal')], 'Type of Operation', required=True)
|
|
return_picking_type_id = fields.Many2one('stock.picking.type', 'Picking Type for Returns')
|
|
show_entire_packs = fields.Boolean('Allow moving packs', help="If checked, this shows the packs to be moved as a whole in the Operations tab all the time, even if there was no entire pack reserved.")
|
|
warehouse_id = fields.Many2one(
|
|
'stock.warehouse', 'Warehouse', ondelete='cascade',
|
|
default=lambda self: self.env['stock.warehouse'].search([('company_id', '=', self.env.user.company_id.id)], limit=1))
|
|
active = fields.Boolean('Active', default=True)
|
|
use_create_lots = fields.Boolean(
|
|
'Create New Lots/Serial Numbers', default=True,
|
|
help="If this is checked only, it will suppose you want to create new Lots/Serial Numbers, so you can provide them in a text field. ")
|
|
use_existing_lots = fields.Boolean(
|
|
'Use Existing Lots/Serial Numbers', default=True,
|
|
help="If this is checked, you will be able to choose the Lots/Serial Numbers. You can also decide to not put lots in this picking type. This means it will create stock with no lot or not put a restriction on the lot taken. ")
|
|
|
|
# Statistics for the kanban view
|
|
last_done_picking = fields.Char('Last 10 Done Pickings', compute='_compute_last_done_picking')
|
|
count_picking_draft = fields.Integer(compute='_compute_picking_count')
|
|
count_picking_ready = fields.Integer(compute='_compute_picking_count')
|
|
count_picking = fields.Integer(compute='_compute_picking_count')
|
|
count_picking_waiting = fields.Integer(compute='_compute_picking_count')
|
|
count_picking_late = fields.Integer(compute='_compute_picking_count')
|
|
count_picking_backorders = fields.Integer(compute='_compute_picking_count')
|
|
rate_picking_late = fields.Integer(compute='_compute_picking_count')
|
|
rate_picking_backorders = fields.Integer(compute='_compute_picking_count')
|
|
|
|
barcode_nomenclature_id = fields.Many2one(
|
|
'barcode.nomenclature', 'Barcode Nomenclature')
|
|
|
|
@api.one
|
|
def _compute_last_done_picking(self):
|
|
# TDE TODO: true multi
|
|
tristates = []
|
|
for picking in self.env['stock.picking'].search([('picking_type_id', '=', self.id), ('state', '=', 'done')], order='date_done desc', limit=10):
|
|
if picking.date_done > picking.date:
|
|
tristates.insert(0, {'tooltip': picking.name or '' + ": " + _('Late'), 'value': -1})
|
|
elif picking.backorder_id:
|
|
tristates.insert(0, {'tooltip': picking.name or '' + ": " + _('Backorder exists'), 'value': 0})
|
|
else:
|
|
tristates.insert(0, {'tooltip': picking.name or '' + ": " + _('OK'), 'value': 1})
|
|
self.last_done_picking = json.dumps(tristates)
|
|
|
|
@api.multi
|
|
def _compute_picking_count(self):
|
|
# TDE TODO count picking can be done using previous two
|
|
domains = {
|
|
'count_picking_draft': [('state', '=', 'draft')],
|
|
'count_picking_waiting': [('state', 'in', ('confirmed', 'waiting'))],
|
|
'count_picking_ready': [('state', 'in', ('assigned', 'partially_available'))],
|
|
'count_picking': [('state', 'in', ('assigned', 'waiting', 'confirmed', 'partially_available'))],
|
|
'count_picking_late': [('min_date', '<', time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)), ('state', 'in', ('assigned', 'waiting', 'confirmed', 'partially_available'))],
|
|
'count_picking_backorders': [('backorder_id', '!=', False), ('state', 'in', ('confirmed', 'assigned', 'waiting', 'partially_available'))],
|
|
}
|
|
for field in domains:
|
|
data = self.env['stock.picking'].read_group(domains[field] +
|
|
[('state', 'not in', ('done', 'cancel')), ('picking_type_id', 'in', self.ids)],
|
|
['picking_type_id'], ['picking_type_id'])
|
|
count = dict(map(lambda x: (x['picking_type_id'] and x['picking_type_id'][0], x['picking_type_id_count']), data))
|
|
for record in self:
|
|
record[field] = count.get(record.id, 0)
|
|
for record in self:
|
|
record.rate_picking_late = record.count_picking and record.count_picking_late * 100 / record.count_picking or 0
|
|
record.rate_picking_backorders = record.count_picking and record.count_picking_backorders * 100 / record.count_picking or 0
|
|
|
|
@api.multi
|
|
def name_get(self):
|
|
""" Display 'Warehouse_name: PickingType_name' """
|
|
# TDE TODO remove context key support + update purchase
|
|
res = []
|
|
for picking_type in self:
|
|
if self.env.context.get('special_shortened_wh_name'):
|
|
if picking_type.warehouse_id:
|
|
name = picking_type.warehouse_id.name
|
|
else:
|
|
name = _('Customer') + ' (' + picking_type.name + ')'
|
|
elif picking_type.warehouse_id:
|
|
name = picking_type.warehouse_id.name + ': ' + picking_type.name
|
|
else:
|
|
name = picking_type.name
|
|
res.append((picking_type.id, name))
|
|
return res
|
|
|
|
@api.model
|
|
def name_search(self, name, args=None, operator='ilike', limit=100):
|
|
args = args or []
|
|
domain = []
|
|
if name:
|
|
domain = ['|', ('name', operator, name), ('warehouse_id.name', operator, name)]
|
|
picks = self.search(domain + args, limit=limit)
|
|
return picks.name_get()
|
|
|
|
@api.onchange('code')
|
|
def onchange_picking_code(self):
|
|
if self.code == 'incoming':
|
|
self.default_location_src_id = self.env.ref('stock.stock_location_suppliers').id
|
|
self.default_location_dest_id = self.env.ref('stock.stock_location_stock').id
|
|
elif self.code == 'outgoing':
|
|
self.default_location_src_id = self.env.ref('stock.stock_location_stock').id
|
|
self.default_location_dest_id = self.env.ref('stock.stock_location_customers').id
|
|
|
|
@api.multi
|
|
def _get_action(self, action_xmlid):
|
|
# TDE TODO check to have one view + custo in methods
|
|
action = self.env.ref(action_xmlid).read()[0]
|
|
if self:
|
|
action['display_name'] = self.display_name
|
|
return action
|
|
|
|
@api.multi
|
|
def get_action_picking_tree_late(self):
|
|
return self._get_action('stock.action_picking_tree_late')
|
|
|
|
@api.multi
|
|
def get_action_picking_tree_backorder(self):
|
|
return self._get_action('stock.action_picking_tree_backorder')
|
|
|
|
@api.multi
|
|
def get_action_picking_tree_waiting(self):
|
|
return self._get_action('stock.action_picking_tree_waiting')
|
|
|
|
@api.multi
|
|
def get_action_picking_tree_ready(self):
|
|
return self._get_action('stock.action_picking_tree_ready')
|
|
|
|
@api.multi
|
|
def get_stock_picking_action_picking_type(self):
|
|
return self._get_action('stock.stock_picking_action_picking_type')
|
|
|
|
|
|
class Picking(models.Model):
|
|
_name = "stock.picking"
|
|
_inherit = ['mail.thread']
|
|
_description = "Transfer"
|
|
_order = "priority desc, date asc, id desc"
|
|
|
|
name = fields.Char(
|
|
'Reference', default='/',
|
|
copy=False, index=True,
|
|
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]})
|
|
origin = fields.Char(
|
|
'Source Document', index=True,
|
|
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
|
|
help="Reference of the document")
|
|
note = fields.Text('Notes')
|
|
|
|
backorder_id = fields.Many2one(
|
|
'stock.picking', 'Back Order of',
|
|
copy=False, index=True,
|
|
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
|
|
help="If this shipment was split, then this field links to the shipment which contains the already processed part.")
|
|
|
|
move_type = fields.Selection([
|
|
('direct', 'Partial'), ('one', 'All at once')], 'Delivery Type',
|
|
default='direct', required=True,
|
|
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
|
|
help="It specifies goods to be deliver partially or all at once")
|
|
|
|
state = fields.Selection([
|
|
('draft', 'Draft'), ('cancel', 'Cancelled'),
|
|
('waiting', 'Waiting Another Operation'),
|
|
('confirmed', 'Waiting Availability'),
|
|
('partially_available', 'Partially Available'),
|
|
('assigned', 'Available'), ('done', 'Done')], string='Status', compute='_compute_state',
|
|
copy=False, index=True, readonly=True, store=True, track_visibility='onchange',
|
|
help=" * Draft: not confirmed yet and will not be scheduled until confirmed\n"
|
|
" * Waiting Another Operation: waiting for another move to proceed before it becomes automatically available (e.g. in Make-To-Order flows)\n"
|
|
" * Waiting Availability: still waiting for the availability of products\n"
|
|
" * Partially Available: some products are available and reserved\n"
|
|
" * Ready to Transfer: products reserved, simply waiting for confirmation.\n"
|
|
" * Transferred: has been processed, can't be modified or cancelled anymore\n"
|
|
" * Cancelled: has been cancelled, can't be confirmed anymore")
|
|
|
|
group_id = fields.Many2one(
|
|
'procurement.group', 'Procurement Group',
|
|
readonly=True, related='move_lines.group_id', store=True)
|
|
|
|
priority = fields.Selection(
|
|
procurement.PROCUREMENT_PRIORITIES, string='Priority',
|
|
compute='_compute_priority', inverse='_set_priority', store=True,
|
|
# default='1', required=True, # TDE: required, depending on moves ? strange
|
|
index=True, track_visibility='onchange',
|
|
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
|
|
help="Priority for this picking. Setting manually a value here would set it as priority for all the moves")
|
|
min_date = fields.Datetime(
|
|
'Scheduled Date', compute='_compute_dates', inverse='_set_min_date', store=True,
|
|
index=True, track_visibility='onchange',
|
|
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
|
|
help="Scheduled time for the first part of the shipment to be processed. Setting manually a value here would set it as expected date for all the stock moves.")
|
|
max_date = fields.Datetime(
|
|
'Max. Expected Date', compute='_compute_dates', store=True,
|
|
index=True,
|
|
help="Scheduled time for the last part of the shipment to be processed")
|
|
date = fields.Datetime(
|
|
'Creation Date',
|
|
default=fields.Datetime.now, index=True, track_visibility='onchange',
|
|
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
|
|
help="Creation Date, usually the time of the order")
|
|
date_done = fields.Datetime('Date of Transfer', copy=False, readonly=True, help="Completion Date of Transfer")
|
|
|
|
location_id = fields.Many2one(
|
|
'stock.location', "Source Location Zone",
|
|
default=lambda self: self.env['stock.picking.type'].browse(self._context.get('default_picking_type_id')).default_location_src_id,
|
|
readonly=True, required=True,
|
|
states={'draft': [('readonly', False)]})
|
|
location_dest_id = fields.Many2one(
|
|
'stock.location', "Destination Location Zone",
|
|
default=lambda self: self.env['stock.picking.type'].browse(self._context.get('default_picking_type_id')).default_location_dest_id,
|
|
readonly=True, required=True,
|
|
states={'draft': [('readonly', False)]})
|
|
move_lines = fields.One2many('stock.move', 'picking_id', string="Stock Moves", copy=True)
|
|
has_scrap_move = fields.Boolean(
|
|
'Has Scrap Moves', compute='_has_scrap_move')
|
|
picking_type_id = fields.Many2one(
|
|
'stock.picking.type', 'Picking Type',
|
|
required=True,
|
|
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]})
|
|
picking_type_code = fields.Selection([
|
|
('incoming', 'Vendors'),
|
|
('outgoing', 'Customers'),
|
|
('internal', 'Internal')], related='picking_type_id.code',
|
|
readonly=True)
|
|
picking_type_entire_packs = fields.Boolean(related='picking_type_id.show_entire_packs',
|
|
readonly=True)
|
|
|
|
quant_reserved_exist = fields.Boolean(
|
|
'Has quants already reserved', compute='_compute_quant_reserved_exist',
|
|
help='Check the existance of quants linked to this picking')
|
|
|
|
partner_id = fields.Many2one(
|
|
'res.partner', 'Partner',
|
|
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]})
|
|
company_id = fields.Many2one(
|
|
'res.company', 'Company',
|
|
default=lambda self: self.env['res.company']._company_default_get('stock.picking'),
|
|
index=True, required=True,
|
|
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]})
|
|
# TDE FIXME: separate those two kind of pack operations
|
|
pack_operation_ids = fields.One2many(
|
|
'stock.pack.operation', 'picking_id', 'Related Packing Operations',
|
|
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]})
|
|
pack_operation_product_ids = fields.One2many(
|
|
'stock.pack.operation', 'picking_id', 'Non pack',
|
|
domain=[('product_id', '!=', False)],
|
|
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]})
|
|
pack_operation_pack_ids = fields.One2many(
|
|
'stock.pack.operation', 'picking_id', 'Pack',
|
|
domain=[('product_id', '=', False)],
|
|
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]})
|
|
|
|
pack_operation_exist = fields.Boolean(
|
|
'Has Pack Operations', compute='_compute_pack_operation_exist',
|
|
help='Check the existence of pack operation on the picking')
|
|
|
|
owner_id = fields.Many2one(
|
|
'res.partner', 'Owner',
|
|
states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
|
|
help="Default Owner")
|
|
printed = fields.Boolean('Printed')
|
|
# Used to search on pickings
|
|
product_id = fields.Many2one('product.product', 'Product', related='move_lines.product_id')
|
|
recompute_pack_op = fields.Boolean(
|
|
'Recompute pack operation?', copy=False,
|
|
help='True if reserved quants changed, which mean we might need to recompute the package operations')
|
|
launch_pack_operations = fields.Boolean("Launch Pack Operations", copy=False)
|
|
|
|
_sql_constraints = [
|
|
('name_uniq', 'unique(name, company_id)', 'Reference must be unique per company!'),
|
|
]
|
|
|
|
@api.depends('move_type', 'launch_pack_operations', 'move_lines.state', 'move_lines.picking_id', 'move_lines.partially_available')
|
|
@api.one
|
|
def _compute_state(self):
|
|
''' State of a picking depends on the state of its related stock.move
|
|
- no moves: draft or assigned (launch_pack_operations)
|
|
- all moves canceled: cancel
|
|
- all moves done (including possible canceled): done
|
|
- All at once picking: least of confirmed / waiting / assigned
|
|
- Partial picking
|
|
- all moves assigned: assigned
|
|
- one of the move is assigned or partially available: partially available
|
|
- otherwise in waiting or confirmed state
|
|
'''
|
|
if not self.move_lines and self.launch_pack_operations:
|
|
self.state = 'assigned'
|
|
elif not self.move_lines:
|
|
self.state = 'draft'
|
|
elif any(move.state == 'draft' for move in self.move_lines): # TDE FIXME: should be all ?
|
|
self.state = 'draft'
|
|
elif all(move.state == 'cancel' for move in self.move_lines):
|
|
self.state = 'cancel'
|
|
elif all(move.state in ['cancel', 'done'] for move in self.move_lines):
|
|
self.state = 'done'
|
|
else:
|
|
# We sort our moves by importance of state: "confirmed" should be first, then we'll have
|
|
# "waiting" and finally "assigned" at the end.
|
|
moves_todo = self.move_lines\
|
|
.filtered(lambda move: move.state not in ['cancel', 'done'])\
|
|
.sorted(key=lambda move: (move.state == 'assigned' and 2) or (move.state == 'waiting' and 1) or 0)
|
|
if self.move_type == 'one':
|
|
self.state = moves_todo[0].state or 'draft'
|
|
elif moves_todo[0].state != 'assigned' and any(x.partially_available or x.state == 'assigned' for x in moves_todo):
|
|
self.state = 'partially_available'
|
|
else:
|
|
self.state = moves_todo[-1].state or 'draft'
|
|
|
|
@api.one
|
|
@api.depends('move_lines.priority')
|
|
def _compute_priority(self):
|
|
self.priority = self.mapped('move_lines') and max(self.mapped('move_lines').mapped('priority')) or '1'
|
|
|
|
@api.one
|
|
def _set_priority(self):
|
|
self.move_lines.write({'priority': self.priority})
|
|
|
|
@api.one
|
|
@api.depends('move_lines.date_expected')
|
|
def _compute_dates(self):
|
|
self.min_date = min(self.move_lines.mapped('date_expected') or [False])
|
|
self.max_date = max(self.move_lines.mapped('date_expected') or [False])
|
|
|
|
@api.one
|
|
def _set_min_date(self):
|
|
self.move_lines.write({'date_expected': self.min_date})
|
|
|
|
@api.one
|
|
def _has_scrap_move(self):
|
|
# TDE FIXME: better implementation
|
|
self.has_scrap_move = bool(self.env['stock.move'].search_count([('picking_id', '=', self.id), ('scrapped', '=', True)]))
|
|
|
|
@api.one
|
|
def _compute_quant_reserved_exist(self):
|
|
# TDE TODO: chould probably be cleaned with a search in quants
|
|
self.quant_reserved_exist = any(move.reserved_quant_ids for move in self.mapped('move_lines'))
|
|
|
|
@api.one
|
|
def _compute_pack_operation_exist(self):
|
|
self.pack_operation_exist = bool(self.pack_operation_ids)
|
|
|
|
@api.onchange('picking_type_id', 'partner_id')
|
|
def onchange_picking_type(self):
|
|
if self.picking_type_id:
|
|
if self.picking_type_id.default_location_src_id:
|
|
location_id = self.picking_type_id.default_location_src_id.id
|
|
elif self.partner_id:
|
|
location_id = self.partner_id.property_stock_supplier.id
|
|
else:
|
|
customerloc, location_id = self.env['stock.warehouse']._get_partner_locations()
|
|
|
|
if self.picking_type_id.default_location_dest_id:
|
|
location_dest_id = self.picking_type_id.default_location_dest_id.id
|
|
elif self.partner_id:
|
|
location_dest_id = self.partner_id.property_stock_customer.id
|
|
else:
|
|
location_dest_id, supplierloc = self.env['stock.warehouse']._get_partner_locations()
|
|
|
|
self.location_id = location_id
|
|
self.location_dest_id = location_dest_id
|
|
# TDE CLEANME move into onchange_partner_id
|
|
if self.partner_id:
|
|
if self.partner_id.picking_warn == 'no-message' and self.partner_id.parent_id:
|
|
partner = self.partner_id.parent_id
|
|
elif self.partner_id.picking_warn not in ('no-message', 'block') and self.partner_id.parent_id.picking_warn == 'block':
|
|
partner = self.partner_id.parent_id
|
|
else:
|
|
partner = self.partner_id
|
|
if partner.picking_warn != 'no-message':
|
|
if partner.picking_warn == 'block':
|
|
self.partner_id = False
|
|
return {'warning': {
|
|
'title': ("Warning for %s") % partner.name,
|
|
'message': partner.picking_warn_msg
|
|
}}
|
|
|
|
@api.model
|
|
def create(self, vals):
|
|
# TDE FIXME: clean that brol
|
|
defaults = self.default_get(['name', 'picking_type_id'])
|
|
if vals.get('name', '/') == '/' and defaults.get('name', '/') == '/' and vals.get('picking_type_id', defaults.get('picking_type_id')):
|
|
vals['name'] = self.env['stock.picking.type'].browse(vals.get('picking_type_id', defaults.get('picking_type_id'))).sequence_id.next_by_id()
|
|
|
|
# TDE FIXME: what ?
|
|
# As the on_change in one2many list is WIP, we will overwrite the locations on the stock moves here
|
|
# As it is a create the format will be a list of (0, 0, dict)
|
|
if vals.get('move_lines') and vals.get('location_id') and vals.get('location_dest_id'):
|
|
for move in vals['move_lines']:
|
|
if len(move) == 3:
|
|
move[2]['location_id'] = vals['location_id']
|
|
move[2]['location_dest_id'] = vals['location_dest_id']
|
|
return super(Picking, self).create(vals)
|
|
|
|
@api.multi
|
|
def write(self, vals):
|
|
res = super(Picking, self).write(vals)
|
|
# Change locations of moves if those of the picking change
|
|
after_vals = {}
|
|
if vals.get('location_id'):
|
|
after_vals['location_id'] = vals['location_id']
|
|
if vals.get('location_dest_id'):
|
|
after_vals['location_dest_id'] = vals['location_dest_id']
|
|
if after_vals:
|
|
self.mapped('move_lines').filtered(lambda move: not move.scrapped).write(after_vals)
|
|
return res
|
|
|
|
@api.multi
|
|
def unlink(self):
|
|
self.mapped('move_lines').action_cancel()
|
|
self.mapped('move_lines').unlink() # Checks if moves are not done
|
|
return super(Picking, self).unlink()
|
|
|
|
# Actions
|
|
# ----------------------------------------
|
|
|
|
@api.one
|
|
def action_assign_owner(self):
|
|
self.pack_operation_ids.write({'owner_id': self.owner_id.id})
|
|
|
|
@api.multi
|
|
def do_print_picking(self):
|
|
self.write({'printed': True})
|
|
return self.env["report"].get_action(self, 'stock.report_picking')
|
|
|
|
@api.multi
|
|
def action_confirm(self):
|
|
self.filtered(lambda picking: not picking.move_lines).write({'launch_pack_operations': True})
|
|
# TDE CLEANME: use of launch pack operation, really useful ?
|
|
self.mapped('move_lines').filtered(lambda move: move.state == 'draft').action_confirm()
|
|
self.filtered(lambda picking: picking.location_id.usage in ('supplier', 'inventory', 'production')).force_assign()
|
|
return True
|
|
|
|
@api.multi
|
|
def action_assign(self):
|
|
""" Check availability of picking moves.
|
|
This has the effect of changing the state and reserve quants on available moves, and may
|
|
also impact the state of the picking as it is computed based on move's states.
|
|
@return: True
|
|
"""
|
|
self.filtered(lambda picking: picking.state == 'draft').action_confirm()
|
|
moves = self.mapped('move_lines').filtered(lambda move: move.state not in ('draft', 'cancel', 'done'))
|
|
if not moves:
|
|
raise UserError(_('Nothing to check the availability for.'))
|
|
moves.action_assign()
|
|
return True
|
|
|
|
@api.multi
|
|
def force_assign(self):
|
|
""" Changes state of picking to available if moves are confirmed or waiting.
|
|
@return: True
|
|
"""
|
|
self.mapped('move_lines').filtered(lambda move: move.state in ['confirmed', 'waiting']).force_assign()
|
|
return True
|
|
|
|
@api.multi
|
|
def action_cancel(self):
|
|
self.mapped('move_lines').action_cancel()
|
|
return True
|
|
|
|
@api.multi
|
|
def action_done(self):
|
|
"""Changes picking state to done by processing the Stock Moves of the Picking
|
|
|
|
Normally that happens when the button "Done" is pressed on a Picking view.
|
|
@return: True
|
|
"""
|
|
# TDE FIXME: remove decorator when migration the remaining
|
|
# TDE FIXME: draft -> automatically done, if waiting ?? CLEAR ME
|
|
draft_moves = self.mapped('move_lines').filtered(lambda self: self.state == 'draft')
|
|
todo_moves = self.mapped('move_lines').filtered(lambda self: self.state in ['draft', 'assigned', 'confirmed'])
|
|
draft_moves.action_confirm()
|
|
todo_moves.action_done()
|
|
return True
|
|
|
|
@api.multi
|
|
def recheck_availability(self):
|
|
self.action_assign()
|
|
self.do_prepare_partial()
|
|
|
|
def _prepare_pack_ops(self, quants, forced_qties):
|
|
""" Prepare pack_operations, returns a list of dict to give at create """
|
|
# TDE CLEANME: oh dear ...
|
|
valid_quants = quants.filtered(lambda quant: quant.qty > 0)
|
|
_Mapping = namedtuple('Mapping', ('product', 'package', 'owner', 'location', 'location_dst_id'))
|
|
|
|
all_products = valid_quants.mapped('product_id') | self.env['product.product'].browse(p.id for p in forced_qties.keys()) | self.move_lines.mapped('product_id')
|
|
computed_putaway_locations = dict(
|
|
(product, self.location_dest_id.get_putaway_strategy(product) or self.location_dest_id.id) for product in all_products)
|
|
|
|
product_to_uom = dict((product.id, product.uom_id) for product in all_products)
|
|
picking_moves = self.move_lines.filtered(lambda move: move.state not in ('done', 'cancel'))
|
|
for move in picking_moves:
|
|
# If we encounter an UoM that is smaller than the default UoM or the one already chosen, use the new one instead.
|
|
if move.product_uom != product_to_uom[move.product_id.id] and move.product_uom.factor > product_to_uom[move.product_id.id].factor:
|
|
product_to_uom[move.product_id.id] = move.product_uom
|
|
if len(picking_moves.mapped('location_id')) > 1:
|
|
raise UserError(_('The source location must be the same for all the moves of the picking.'))
|
|
if len(picking_moves.mapped('location_dest_id')) > 1:
|
|
raise UserError(_('The destination location must be the same for all the moves of the picking.'))
|
|
|
|
pack_operation_values = []
|
|
# find the packages we can move as a whole, create pack operations and mark related quants as done
|
|
top_lvl_packages = valid_quants._get_top_level_packages(computed_putaway_locations)
|
|
for pack in top_lvl_packages:
|
|
pack_quants = pack.get_content()
|
|
pack_operation_values.append({
|
|
'picking_id': self.id,
|
|
'package_id': pack.id,
|
|
'product_qty': 1.0,
|
|
'location_id': pack.location_id.id,
|
|
'location_dest_id': computed_putaway_locations[pack_quants[0].product_id],
|
|
'owner_id': pack.owner_id.id,
|
|
})
|
|
valid_quants -= pack_quants
|
|
|
|
# Go through all remaining reserved quants and group by product, package, owner, source location and dest location
|
|
# Lots will go into pack operation lot object
|
|
qtys_grouped = {}
|
|
lots_grouped = {}
|
|
for quant in valid_quants:
|
|
key = _Mapping(quant.product_id, quant.package_id, quant.owner_id, quant.location_id, computed_putaway_locations[quant.product_id])
|
|
qtys_grouped.setdefault(key, 0.0)
|
|
qtys_grouped[key] += quant.qty
|
|
if quant.product_id.tracking != 'none' and quant.lot_id:
|
|
lots_grouped.setdefault(key, dict()).setdefault(quant.lot_id.id, 0.0)
|
|
lots_grouped[key][quant.lot_id.id] += quant.qty
|
|
# Do the same for the forced quantities (in cases of force_assign or incomming shipment for example)
|
|
for product, qty in forced_qties.items():
|
|
if qty <= 0.0:
|
|
continue
|
|
key = _Mapping(product, self.env['stock.quant.package'], self.owner_id, self.location_id, computed_putaway_locations[product])
|
|
qtys_grouped.setdefault(key, 0.0)
|
|
qtys_grouped[key] += qty
|
|
|
|
# Create the necessary operations for the grouped quants and remaining qtys
|
|
Uom = self.env['product.uom']
|
|
product_id_to_vals = {} # use it to create operations using the same order as the picking stock moves
|
|
for mapping, qty in qtys_grouped.items():
|
|
uom = product_to_uom[mapping.product.id]
|
|
val_dict = {
|
|
'picking_id': self.id,
|
|
'product_qty': mapping.product.uom_id._compute_quantity(qty, uom),
|
|
'product_id': mapping.product.id,
|
|
'package_id': mapping.package.id,
|
|
'owner_id': mapping.owner.id,
|
|
'location_id': mapping.location.id,
|
|
'location_dest_id': mapping.location_dst_id,
|
|
'product_uom_id': uom.id,
|
|
'pack_lot_ids': [
|
|
(0, 0, {
|
|
'lot_id': lot,
|
|
'qty': 0.0,
|
|
'qty_todo': mapping.product.uom_id._compute_quantity(lots_grouped[mapping][lot], uom)
|
|
}) for lot in lots_grouped.get(mapping, {}).keys()],
|
|
}
|
|
product_id_to_vals.setdefault(mapping.product.id, list()).append(val_dict)
|
|
|
|
for move in self.move_lines.filtered(lambda move: move.state not in ('done', 'cancel')):
|
|
values = product_id_to_vals.pop(move.product_id.id, [])
|
|
pack_operation_values += values
|
|
return pack_operation_values
|
|
|
|
@api.multi
|
|
def do_prepare_partial(self):
|
|
# TDE CLEANME: oh dear ...
|
|
PackOperation = self.env['stock.pack.operation']
|
|
|
|
# get list of existing operations and delete them
|
|
existing_packages = PackOperation.search([('picking_id', 'in', self.ids)]) # TDE FIXME: o2m / m2o ?
|
|
if existing_packages:
|
|
existing_packages.unlink()
|
|
for picking in self:
|
|
forced_qties = {} # Quantity remaining after calculating reserved quants
|
|
picking_quants = self.env['stock.quant']
|
|
# Calculate packages, reserved quants, qtys of this picking's moves
|
|
for move in picking.move_lines:
|
|
if move.state not in ('assigned', 'confirmed', 'waiting'):
|
|
continue
|
|
move_quants = move.reserved_quant_ids
|
|
picking_quants += move_quants
|
|
forced_qty = 0.0
|
|
if move.state == 'assigned':
|
|
qty = move.product_uom._compute_quantity(move.product_uom_qty, move.product_id.uom_id, round=False)
|
|
forced_qty = qty - sum([x.qty for x in move_quants])
|
|
# if we used force_assign() on the move, or if the move is incoming, forced_qty > 0
|
|
if float_compare(forced_qty, 0, precision_rounding=move.product_id.uom_id.rounding) > 0:
|
|
if forced_qties.get(move.product_id):
|
|
forced_qties[move.product_id] += forced_qty
|
|
else:
|
|
forced_qties[move.product_id] = forced_qty
|
|
for vals in picking._prepare_pack_ops(picking_quants, forced_qties):
|
|
vals['fresh_record'] = False
|
|
PackOperation |= PackOperation.create(vals)
|
|
# recompute the remaining quantities all at once
|
|
self.do_recompute_remaining_quantities()
|
|
for pack in PackOperation:
|
|
pack.ordered_qty = sum(
|
|
pack.mapped('linked_move_operation_ids').mapped('move_id').filtered(lambda r: r.state != 'cancel').mapped('ordered_qty')
|
|
)
|
|
self.write({'recompute_pack_op': False})
|
|
|
|
@api.multi
|
|
def do_unreserve(self):
|
|
"""
|
|
Will remove all quants for picking in picking_ids
|
|
"""
|
|
moves_to_unreserve = self.mapped('move_lines').filtered(lambda move: move.state not in ('done', 'cancel'))
|
|
pack_line_to_unreserve = self.mapped('pack_operation_ids')
|
|
if moves_to_unreserve:
|
|
if pack_line_to_unreserve:
|
|
pack_line_to_unreserve.unlink()
|
|
moves_to_unreserve.do_unreserve()
|
|
|
|
def recompute_remaining_qty(self, done_qtys=False):
|
|
|
|
def _create_link_for_index(operation_id, index, product_id, qty_to_assign, quant_id=False):
|
|
move_dict = prod2move_ids[product_id][index]
|
|
qty_on_link = min(move_dict['remaining_qty'], qty_to_assign)
|
|
self.env['stock.move.operation.link'].create({'move_id': move_dict['move'].id, 'operation_id': operation_id, 'qty': qty_on_link, 'reserved_quant_id': quant_id})
|
|
if float_compare(move_dict['remaining_qty'], qty_on_link, precision_rounding=move_dict['move'].product_uom.rounding) == 0:
|
|
prod2move_ids[product_id].pop(index)
|
|
else:
|
|
move_dict['remaining_qty'] -= qty_on_link
|
|
return qty_on_link
|
|
|
|
def _create_link_for_quant(operation_id, quant, qty):
|
|
"""create a link for given operation and reserved move of given quant, for the max quantity possible, and returns this quantity"""
|
|
if not quant.reservation_id.id:
|
|
return _create_link_for_product(operation_id, quant.product_id.id, qty)
|
|
qty_on_link = 0
|
|
for i in range(0, len(prod2move_ids[quant.product_id.id])):
|
|
if prod2move_ids[quant.product_id.id][i]['move'].id != quant.reservation_id.id:
|
|
continue
|
|
qty_on_link = _create_link_for_index(operation_id, i, quant.product_id.id, qty, quant_id=quant.id)
|
|
break
|
|
return qty_on_link
|
|
|
|
def _create_link_for_product(operation_id, product_id, qty):
|
|
'''method that creates the link between a given operation and move(s) of given product, for the given quantity.
|
|
Returns True if it was possible to create links for the requested quantity (False if there was not enough quantity on stock moves)'''
|
|
qty_to_assign = qty
|
|
Product = self.env["product.product"]
|
|
product = Product.browse(product_id)
|
|
rounding = product.uom_id.rounding
|
|
qtyassign_cmp = float_compare(qty_to_assign, 0.0, precision_rounding=rounding)
|
|
if prod2move_ids.get(product_id):
|
|
while prod2move_ids[product_id] and qtyassign_cmp > 0:
|
|
qty_on_link = _create_link_for_index(operation_id, 0, product_id, qty_to_assign, quant_id=False)
|
|
qty_to_assign -= qty_on_link
|
|
qtyassign_cmp = float_compare(qty_to_assign, 0.0, precision_rounding=rounding)
|
|
return qtyassign_cmp == 0
|
|
|
|
# TDE CLEANME: oh dear ...
|
|
Uom = self.env['product.uom']
|
|
QuantPackage = self.env['stock.quant.package']
|
|
OperationLink = self.env['stock.move.operation.link']
|
|
|
|
quants_in_package_done = set()
|
|
prod2move_ids = {}
|
|
still_to_do = []
|
|
# make a dictionary giving for each product, the moves and related quantity that can be used in operation links
|
|
moves = sorted([x for x in self.move_lines if x.state not in ('done', 'cancel')], key=lambda x: (((x.state == 'assigned') and -2 or 0) + (x.partially_available and -1 or 0)))
|
|
for move in moves:
|
|
if not prod2move_ids.get(move.product_id.id):
|
|
prod2move_ids[move.product_id.id] = [{'move': move, 'remaining_qty': move.product_qty}]
|
|
else:
|
|
prod2move_ids[move.product_id.id].append({'move': move, 'remaining_qty': move.product_qty})
|
|
|
|
need_rereserve = False
|
|
# sort the operations in order to give higher priority to those with a package, then a lot/serial number
|
|
operations = self.pack_operation_ids
|
|
operations = sorted(operations, key=lambda x: ((x.package_id and not x.product_id) and -4 or 0) + (x.package_id and -2 or 0) + (x.pack_lot_ids and -1 or 0))
|
|
# delete existing operations to start again from scratch
|
|
links = OperationLink.search([('operation_id', 'in', [x.id for x in operations])])
|
|
if links:
|
|
links.unlink()
|
|
# 1) first, try to create links when quants can be identified without any doubt
|
|
for ops in operations:
|
|
lot_qty = {}
|
|
for packlot in ops.pack_lot_ids:
|
|
lot_qty[packlot.lot_id.id] = ops.product_uom_id._compute_quantity(packlot.qty, ops.product_id.uom_id)
|
|
# for each operation, create the links with the stock move by seeking on the matching reserved quants,
|
|
# and deffer the operation if there is some ambiguity on the move to select
|
|
if ops.package_id and not ops.product_id and (not done_qtys or ops.qty_done):
|
|
# entire package
|
|
for quant in ops.package_id.get_content():
|
|
remaining_qty_on_quant = quant.qty
|
|
if quant.reservation_id:
|
|
# avoid quants being counted twice
|
|
quants_in_package_done.add(quant.id)
|
|
qty_on_link = _create_link_for_quant(ops.id, quant, quant.qty)
|
|
remaining_qty_on_quant -= qty_on_link
|
|
if remaining_qty_on_quant:
|
|
still_to_do.append((ops, quant.product_id.id, remaining_qty_on_quant))
|
|
need_rereserve = True
|
|
elif ops.product_id.id:
|
|
# Check moves with same product
|
|
product_qty = ops.qty_done if done_qtys else ops.product_qty
|
|
qty_to_assign = ops.product_uom_id._compute_quantity(product_qty, ops.product_id.uom_id)
|
|
precision_rounding = ops.product_id.uom_id.rounding
|
|
for move_dict in prod2move_ids.get(ops.product_id.id, []):
|
|
move = move_dict['move']
|
|
for quant in move.reserved_quant_ids:
|
|
if float_compare(qty_to_assign, 0, precision_rounding=precision_rounding) != 1:
|
|
break
|
|
if quant.id in quants_in_package_done:
|
|
continue
|
|
|
|
# check if the quant is matching the operation details
|
|
if ops.package_id:
|
|
flag = quant.package_id == ops.package_id
|
|
else:
|
|
flag = not quant.package_id.id
|
|
flag = flag and (ops.owner_id.id == quant.owner_id.id)
|
|
if flag:
|
|
if not lot_qty:
|
|
max_qty_on_link = min(quant.qty, qty_to_assign)
|
|
qty_on_link = _create_link_for_quant(ops.id, quant, max_qty_on_link)
|
|
qty_to_assign -= qty_on_link
|
|
else:
|
|
if lot_qty.get(quant.lot_id.id): # if there is still some qty left
|
|
max_qty_on_link = min(quant.qty, qty_to_assign, lot_qty[quant.lot_id.id])
|
|
qty_on_link = _create_link_for_quant(ops.id, quant, max_qty_on_link)
|
|
qty_to_assign -= qty_on_link
|
|
lot_qty[quant.lot_id.id] -= qty_on_link
|
|
|
|
qty_assign_cmp = float_compare(qty_to_assign, 0, precision_rounding=precision_rounding)
|
|
if qty_assign_cmp > 0:
|
|
# qty reserved is less than qty put in operations. We need to create a link but it's deferred after we processed
|
|
# all the quants (because they leave no choice on their related move and needs to be processed with higher priority)
|
|
still_to_do += [(ops, ops.product_id.id, qty_to_assign)]
|
|
need_rereserve = True
|
|
|
|
# 2) then, process the remaining part
|
|
all_op_processed = True
|
|
for ops, product_id, remaining_qty in still_to_do:
|
|
all_op_processed = _create_link_for_product(ops.id, product_id, remaining_qty) and all_op_processed
|
|
return (need_rereserve, all_op_processed)
|
|
|
|
def picking_recompute_remaining_quantities(self, done_qtys=False):
|
|
need_rereserve = False
|
|
all_op_processed = True
|
|
if self.pack_operation_ids:
|
|
need_rereserve, all_op_processed = self.recompute_remaining_qty(done_qtys=done_qtys)
|
|
return need_rereserve, all_op_processed
|
|
|
|
def do_recompute_remaining_quantities(self, done_qtys=False):
|
|
# TDE FIXME
|
|
tmp = self.filtered(lambda picking: picking.pack_operation_ids)
|
|
if tmp:
|
|
for pick in tmp:
|
|
pick.recompute_remaining_qty(done_qtys=done_qtys)
|
|
|
|
def rereserve_quants(self, move_ids=[]):
|
|
""" Unreserve quants then try to reassign quants."""
|
|
if not move_ids:
|
|
self.do_unreserve()
|
|
self.action_assign()
|
|
else:
|
|
moves = self.env['stock.move'].browse(move_ids)
|
|
if self.env.context.get('no_state_change'):
|
|
moves = moves.filtered(lambda m: m.reserved_quant_ids)
|
|
moves.do_unreserve()
|
|
moves.action_assign(no_prepare=True)
|
|
|
|
@api.multi
|
|
def do_new_transfer(self):
|
|
for pick in self:
|
|
if pick.state == 'done':
|
|
raise UserError(_('The pick is already validated'))
|
|
pack_operations_delete = self.env['stock.pack.operation']
|
|
if not pick.move_lines and not pick.pack_operation_ids:
|
|
raise UserError(_('Please create some Initial Demand or Mark as Todo and create some Operations. '))
|
|
# In draft or with no pack operations edited yet, ask if we can just do everything
|
|
if pick.state == 'draft' or all([x.qty_done == 0.0 for x in pick.pack_operation_ids]):
|
|
# If no lots when needed, raise error
|
|
picking_type = pick.picking_type_id
|
|
if (picking_type.use_create_lots or picking_type.use_existing_lots):
|
|
for pack in pick.pack_operation_ids:
|
|
if pack.product_id and pack.product_id.tracking != 'none':
|
|
raise UserError(_('Some products require lots/serial numbers, so you need to specify those first!'))
|
|
view = self.env.ref('stock.view_immediate_transfer')
|
|
wiz = self.env['stock.immediate.transfer'].create({'pick_id': pick.id})
|
|
# TDE FIXME: a return in a loop, what a good idea. Really.
|
|
return {
|
|
'name': _('Immediate Transfer?'),
|
|
'type': 'ir.actions.act_window',
|
|
'view_type': 'form',
|
|
'view_mode': 'form',
|
|
'res_model': 'stock.immediate.transfer',
|
|
'views': [(view.id, 'form')],
|
|
'view_id': view.id,
|
|
'target': 'new',
|
|
'res_id': wiz.id,
|
|
'context': self.env.context,
|
|
}
|
|
|
|
# Check backorder should check for other barcodes
|
|
if pick.check_backorder():
|
|
view = self.env.ref('stock.view_backorder_confirmation')
|
|
wiz = self.env['stock.backorder.confirmation'].create({'pick_id': pick.id})
|
|
# TDE FIXME: same reamrk as above actually
|
|
return {
|
|
'name': _('Create Backorder?'),
|
|
'type': 'ir.actions.act_window',
|
|
'view_type': 'form',
|
|
'view_mode': 'form',
|
|
'res_model': 'stock.backorder.confirmation',
|
|
'views': [(view.id, 'form')],
|
|
'view_id': view.id,
|
|
'target': 'new',
|
|
'res_id': wiz.id,
|
|
'context': self.env.context,
|
|
}
|
|
for operation in pick.pack_operation_ids:
|
|
if operation.qty_done < 0:
|
|
raise UserError(_('No negative quantities allowed'))
|
|
if operation.qty_done > 0:
|
|
operation.write({'product_qty': operation.qty_done})
|
|
else:
|
|
pack_operations_delete |= operation
|
|
if pack_operations_delete:
|
|
pack_operations_delete.unlink()
|
|
self.do_transfer()
|
|
return
|
|
|
|
def check_backorder(self):
|
|
need_rereserve, all_op_processed = self.picking_recompute_remaining_quantities(done_qtys=True)
|
|
for move in self.move_lines:
|
|
if float_compare(move.remaining_qty, 0, precision_rounding=move.product_id.uom_id.rounding) != 0:
|
|
return True
|
|
return False
|
|
|
|
@api.multi
|
|
def do_transfer(self):
|
|
""" If no pack operation, we do simple action_done of the picking.
|
|
Otherwise, do the pack operations. """
|
|
# TDE CLEAN ME: reclean me, please
|
|
self._create_lots_for_picking()
|
|
|
|
no_pack_op_pickings = self.filtered(lambda picking: not picking.pack_operation_ids)
|
|
no_pack_op_pickings.action_done()
|
|
other_pickings = self - no_pack_op_pickings
|
|
for picking in other_pickings:
|
|
need_rereserve, all_op_processed = picking.picking_recompute_remaining_quantities()
|
|
todo_moves = self.env['stock.move']
|
|
toassign_moves = self.env['stock.move']
|
|
|
|
# create extra moves in the picking (unexpected product moves coming from pack operations)
|
|
if not all_op_processed:
|
|
todo_moves |= picking._create_extra_moves()
|
|
|
|
if need_rereserve or not all_op_processed:
|
|
moves_reassign = any(x.origin_returned_move_id or x.move_orig_ids for x in picking.move_lines if x.state not in ['done', 'cancel'])
|
|
if moves_reassign and picking.location_id.usage not in ("supplier", "production", "inventory"):
|
|
# unnecessary to assign other quants than those involved with pack operations as they will be unreserved anyways.
|
|
picking.with_context(reserve_only_ops=True, no_state_change=True).rereserve_quants(move_ids=picking.move_lines.ids)
|
|
picking.do_recompute_remaining_quantities()
|
|
|
|
# split move lines if needed
|
|
for move in picking.move_lines:
|
|
rounding = move.product_id.uom_id.rounding
|
|
remaining_qty = move.remaining_qty
|
|
if move.state in ('done', 'cancel'):
|
|
# ignore stock moves cancelled or already done
|
|
continue
|
|
elif move.state == 'draft':
|
|
toassign_moves |= move
|
|
if float_compare(remaining_qty, 0, precision_rounding=rounding) == 0:
|
|
if move.state in ('draft', 'assigned', 'confirmed'):
|
|
todo_moves |= move
|
|
elif float_compare(remaining_qty, 0, precision_rounding=rounding) > 0 and float_compare(remaining_qty, move.product_qty, precision_rounding=rounding) < 0:
|
|
# TDE FIXME: shoudl probably return a move - check for no track key, by the way
|
|
new_move_id = move.split(remaining_qty)
|
|
new_move = self.env['stock.move'].with_context(mail_notrack=True).browse(new_move_id)
|
|
todo_moves |= move
|
|
# Assign move as it was assigned before
|
|
toassign_moves |= new_move
|
|
|
|
# TDE FIXME: do_only_split does not seem used anymore
|
|
if todo_moves and not self.env.context.get('do_only_split'):
|
|
todo_moves.action_done()
|
|
elif self.env.context.get('do_only_split'):
|
|
picking = picking.with_context(split=todo_moves.ids)
|
|
|
|
picking._create_backorder()
|
|
return True
|
|
|
|
def _create_lots_for_picking(self):
|
|
Lot = self.env['stock.production.lot']
|
|
for pack_op_lot in self.mapped('pack_operation_ids').mapped('pack_lot_ids'):
|
|
if not pack_op_lot.lot_id:
|
|
lot = Lot.create({'name': pack_op_lot.lot_name, 'product_id': pack_op_lot.operation_id.product_id.id})
|
|
pack_op_lot.write({'lot_id': lot.id})
|
|
# TDE FIXME: this should not be done here
|
|
self.mapped('pack_operation_ids').mapped('pack_lot_ids').filtered(lambda op_lot: op_lot.qty == 0.0).unlink()
|
|
create_lots_for_picking = _create_lots_for_picking
|
|
|
|
def _create_extra_moves(self):
|
|
'''This function creates move lines on a picking, at the time of do_transfer, based on
|
|
unexpected product transfers (or exceeding quantities) found in the pack operations.
|
|
'''
|
|
# TDE FIXME: move to batch
|
|
self.ensure_one()
|
|
moves = self.env['stock.move']
|
|
for pack_operation in self.pack_operation_ids:
|
|
for product, remaining_qty in pack_operation._get_remaining_prod_quantities().items():
|
|
if float_compare(remaining_qty, 0, precision_rounding=product.uom_id.rounding) > 0:
|
|
vals = self._prepare_values_extra_move(pack_operation, product, remaining_qty)
|
|
moves |= moves.create(vals)
|
|
if moves:
|
|
moves.with_context(skip_check=True).action_confirm()
|
|
return moves
|
|
|
|
@api.model
|
|
def _prepare_values_extra_move(self, op, product, remaining_qty):
|
|
"""
|
|
Creates an extra move when there is no corresponding original move to be copied
|
|
"""
|
|
Uom = self.env["product.uom"]
|
|
uom_id = product.uom_id.id
|
|
qty = remaining_qty
|
|
if op.product_id and op.product_uom_id and op.product_uom_id.id != product.uom_id.id:
|
|
if op.product_uom_id.factor > product.uom_id.factor: # If the pack operation's is a smaller unit
|
|
uom_id = op.product_uom_id.id
|
|
# HALF-UP rounding as only rounding errors will be because of propagation of error from default UoM
|
|
qty = product.uom_id._compute_quantity(remaining_qty, op.product_uom_id, rounding_method='HALF-UP')
|
|
picking = op.picking_id
|
|
ref = product.default_code
|
|
name = '[' + ref + ']' + ' ' + product.name if ref else product.name
|
|
proc_id = False
|
|
for m in op.linked_move_operation_ids:
|
|
if m.move_id.procurement_id:
|
|
proc_id = m.move_id.procurement_id.id
|
|
break
|
|
return {
|
|
'picking_id': picking.id,
|
|
'location_id': picking.location_id.id,
|
|
'location_dest_id': picking.location_dest_id.id,
|
|
'product_id': product.id,
|
|
'procurement_id': proc_id,
|
|
'product_uom': uom_id,
|
|
'product_uom_qty': qty,
|
|
'name': _('Extra Move: ') + name,
|
|
'state': 'draft',
|
|
'restrict_partner_id': op.owner_id.id,
|
|
'group_id': picking.group_id.id,
|
|
}
|
|
|
|
@api.multi
|
|
def _create_backorder(self, backorder_moves=[]):
|
|
""" Move all non-done lines into a new backorder picking. If the key 'do_only_split' is given in the context, then move all lines not in context.get('split', []) instead of all non-done lines.
|
|
"""
|
|
# TDE note: o2o conversion, todo multi
|
|
backorders = self.env['stock.picking']
|
|
for picking in self:
|
|
backorder_moves = backorder_moves or picking.move_lines
|
|
if self._context.get('do_only_split'):
|
|
not_done_bo_moves = backorder_moves.filtered(lambda move: move.id not in self._context.get('split', []))
|
|
else:
|
|
not_done_bo_moves = backorder_moves.filtered(lambda move: move.state not in ('done', 'cancel'))
|
|
if not not_done_bo_moves:
|
|
continue
|
|
backorder_picking = picking.copy({
|
|
'name': '/',
|
|
'move_lines': [],
|
|
'pack_operation_ids': [],
|
|
'backorder_id': picking.id
|
|
})
|
|
picking.message_post(body=_("Back order <em>%s</em> <b>created</b>.") % (backorder_picking.name))
|
|
not_done_bo_moves.write({'picking_id': backorder_picking.id})
|
|
if not picking.date_done:
|
|
picking.write({'date_done': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)})
|
|
backorder_picking.action_confirm()
|
|
backorder_picking.action_assign()
|
|
backorders |= backorder_picking
|
|
return backorders
|
|
|
|
@api.multi
|
|
def put_in_pack(self):
|
|
# TDE FIXME: reclean me
|
|
QuantPackage = self.env["stock.quant.package"]
|
|
package = False
|
|
for pick in self:
|
|
operations = [x for x in pick.pack_operation_ids if x.qty_done > 0 and (not x.result_package_id)]
|
|
pack_operation_ids = self.env['stock.pack.operation']
|
|
for operation in operations:
|
|
# If we haven't done all qty in operation, we have to split into 2 operation
|
|
op = operation
|
|
if operation.qty_done < operation.product_qty:
|
|
new_operation = operation.copy({'product_qty': operation.qty_done,'qty_done': operation.qty_done})
|
|
|
|
operation.write({'product_qty': operation.product_qty - operation.qty_done,'qty_done': 0})
|
|
if operation.pack_lot_ids:
|
|
packlots_transfer = [(4, x.id) for x in operation.pack_lot_ids]
|
|
new_operation.write({'pack_lot_ids': packlots_transfer})
|
|
|
|
# the stock.pack.operation.lot records now belong to the new, packaged stock.pack.operation
|
|
# we have to create new ones with new quantities for our original, unfinished stock.pack.operation
|
|
new_operation._copy_remaining_pack_lot_ids(operation)
|
|
|
|
op = new_operation
|
|
pack_operation_ids |= op
|
|
if operations:
|
|
pack_operation_ids.check_tracking()
|
|
package = QuantPackage.create({})
|
|
pack_operation_ids.write({'result_package_id': package.id})
|
|
else:
|
|
raise UserError(_('Please process some quantities to put in the pack first!'))
|
|
return package
|
|
|
|
@api.multi
|
|
def button_scrap(self):
|
|
self.ensure_one()
|
|
return {
|
|
'name': _('Scrap'),
|
|
'view_type': 'form',
|
|
'view_mode': 'form',
|
|
'res_model': 'stock.scrap',
|
|
'view_id': self.env.ref('stock.stock_scrap_form_view2').id,
|
|
'type': 'ir.actions.act_window',
|
|
'context': {'default_picking_id': self.id, 'product_ids': self.pack_operation_product_ids.mapped('product_id').ids},
|
|
'target': 'new',
|
|
}
|
|
|
|
@api.multi
|
|
def action_see_move_scrap(self):
|
|
self.ensure_one()
|
|
action = self.env.ref('stock.action_stock_scrap').read()[0]
|
|
scraps = self.env['stock.scrap'].search([('picking_id', '=', self.id)])
|
|
action['domain'] = [('id', 'in', scraps.ids)]
|
|
return action
|