505 lines
32 KiB
Python
505 lines
32 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||
|
|
||
|
from collections import Counter
|
||
|
|
||
|
from odoo import api, fields, models, _
|
||
|
from odoo.addons import decimal_precision as dp
|
||
|
from odoo.exceptions import UserError, ValidationError
|
||
|
from odoo.tools.pycompat import izip
|
||
|
from odoo.tools.float_utils import float_round, float_compare, float_is_zero
|
||
|
|
||
|
|
||
|
class StockMoveLine(models.Model):
|
||
|
_name = "stock.move.line"
|
||
|
_description = "Packing Operation"
|
||
|
_rec_name = "product_id"
|
||
|
_order = "result_package_id desc, id"
|
||
|
|
||
|
picking_id = fields.Many2one(
|
||
|
'stock.picking', 'Stock Picking',
|
||
|
help='The stock operation where the packing has been made')
|
||
|
move_id = fields.Many2one(
|
||
|
'stock.move', 'Stock Move',
|
||
|
help="Change to a better name")
|
||
|
product_id = fields.Many2one('product.product', 'Product', ondelete="cascade")
|
||
|
product_uom_id = fields.Many2one('product.uom', 'Unit of Measure', required=True)
|
||
|
product_qty = fields.Float(
|
||
|
'Real Reserved Quantity', digits=0,
|
||
|
compute='_compute_product_qty', inverse='_set_product_qty', store=True)
|
||
|
product_uom_qty = fields.Float('Reserved', default=0.0, digits=dp.get_precision('Product Unit of Measure'), required=True)
|
||
|
ordered_qty = fields.Float('Ordered Quantity', digits=dp.get_precision('Product Unit of Measure'))
|
||
|
qty_done = fields.Float('Done', default=0.0, digits=dp.get_precision('Product Unit of Measure'), copy=False)
|
||
|
package_id = fields.Many2one('stock.quant.package', 'Source Package', ondelete='restrict')
|
||
|
lot_id = fields.Many2one('stock.production.lot', 'Lot')
|
||
|
lot_name = fields.Char('Lot/Serial Number')
|
||
|
result_package_id = fields.Many2one(
|
||
|
'stock.quant.package', 'Destination Package',
|
||
|
ondelete='restrict', required=False,
|
||
|
help="If set, the operations are packed into this package")
|
||
|
date = fields.Datetime('Date', default=fields.Datetime.now, required=True)
|
||
|
owner_id = fields.Many2one('res.partner', 'Owner', help="Owner of the quants")
|
||
|
location_id = fields.Many2one('stock.location', 'From', required=True)
|
||
|
location_dest_id = fields.Many2one('stock.location', 'To', required=True)
|
||
|
from_loc = fields.Char(compute='_compute_location_description')
|
||
|
to_loc = fields.Char(compute='_compute_location_description')
|
||
|
lots_visible = fields.Boolean(compute='_compute_lots_visible')
|
||
|
state = fields.Selection(related='move_id.state', store=True)
|
||
|
is_initial_demand_editable = fields.Boolean(related='move_id.is_initial_demand_editable')
|
||
|
is_locked = fields.Boolean(related='move_id.is_locked', default=True, readonly=True)
|
||
|
consume_line_ids = fields.Many2many('stock.move.line', 'stock_move_line_consume_rel', 'consume_line_id', 'produce_line_id', help="Technical link to see who consumed what. ")
|
||
|
produce_line_ids = fields.Many2many('stock.move.line', 'stock_move_line_consume_rel', 'produce_line_id', 'consume_line_id', help="Technical link to see which line was produced with this. ")
|
||
|
reference = fields.Char(related='move_id.reference', store=True)
|
||
|
in_entire_package = fields.Boolean(compute='_compute_in_entire_package')
|
||
|
|
||
|
@api.one
|
||
|
def _compute_location_description(self):
|
||
|
for operation, operation_sudo in izip(self, self.sudo()):
|
||
|
operation.from_loc = '%s%s' % (operation_sudo.location_id.name, operation.product_id and operation_sudo.package_id.name or '')
|
||
|
operation.to_loc = '%s%s' % (operation_sudo.location_dest_id.name, operation_sudo.result_package_id.name or '')
|
||
|
|
||
|
@api.one
|
||
|
@api.depends('picking_id.picking_type_id', 'product_id.tracking')
|
||
|
def _compute_lots_visible(self):
|
||
|
picking = self.picking_id
|
||
|
if picking.picking_type_id and self.product_id.tracking != 'none': # TDE FIXME: not sure correctly migrated
|
||
|
self.lots_visible = picking.picking_type_id.use_existing_lots or picking.picking_type_id.use_create_lots
|
||
|
else:
|
||
|
self.lots_visible = self.product_id.tracking != 'none'
|
||
|
|
||
|
@api.one
|
||
|
@api.depends('product_id', 'product_uom_id', 'product_uom_qty')
|
||
|
def _compute_product_qty(self):
|
||
|
self.product_qty = self.product_uom_id._compute_quantity(self.product_uom_qty, self.product_id.uom_id, rounding_method='HALF-UP')
|
||
|
|
||
|
@api.one
|
||
|
def _set_product_qty(self):
|
||
|
""" The meaning of product_qty field changed lately and is now a functional field computing the quantity
|
||
|
in the default product UoM. This code has been added to raise an error if a write is made given a value
|
||
|
for `product_qty`, where the same write should set the `product_uom_qty` field instead, in order to
|
||
|
detect errors. """
|
||
|
raise UserError(_('The requested operation cannot be processed because of a programming error setting the `product_qty` field instead of the `product_uom_qty`.'))
|
||
|
|
||
|
def _compute_in_entire_package(self):
|
||
|
""" This method check if the move line is in an entire pack shown in the picking."""
|
||
|
for ml in self:
|
||
|
picking_id = ml.picking_id
|
||
|
ml.in_entire_package = picking_id and picking_id.picking_type_entire_packs and picking_id.state != 'done'\
|
||
|
and ml.result_package_id and ml.result_package_id in picking_id.entire_package_ids
|
||
|
|
||
|
@api.constrains('product_uom_qty')
|
||
|
def check_reserved_done_quantity(self):
|
||
|
for move_line in self:
|
||
|
if move_line.state == 'done' and not float_is_zero(move_line.product_uom_qty, precision_rounding=self.env['decimal.precision'].precision_get('Product Unit of Measure')):
|
||
|
raise ValidationError(_('A done move line should never have a reserved quantity.'))
|
||
|
|
||
|
@api.onchange('product_id', 'product_uom_id')
|
||
|
def onchange_product_id(self):
|
||
|
if self.product_id:
|
||
|
self.lots_visible = self.product_id.tracking != 'none'
|
||
|
if not self.product_uom_id or self.product_uom_id.category_id != self.product_id.uom_id.category_id:
|
||
|
if self.move_id.product_uom:
|
||
|
self.product_uom_id = self.move_id.product_uom.id
|
||
|
else:
|
||
|
self.product_uom_id = self.product_id.uom_id.id
|
||
|
res = {'domain': {'product_uom_id': [('category_id', '=', self.product_uom_id.category_id.id)]}}
|
||
|
else:
|
||
|
res = {'domain': {'product_uom_id': []}}
|
||
|
return res
|
||
|
|
||
|
@api.onchange('lot_name', 'lot_id')
|
||
|
def onchange_serial_number(self):
|
||
|
res = {}
|
||
|
if self.product_id.tracking == 'serial':
|
||
|
self.qty_done = 1
|
||
|
move_lines_to_check = self._get_similar_move_lines() - self
|
||
|
message = move_lines_to_check._check_for_duplicated_serial_numbers()
|
||
|
if message:
|
||
|
res['warning'] = {'title': _('Warning'), 'message': message}
|
||
|
return res
|
||
|
|
||
|
@api.constrains('qty_done')
|
||
|
def _check_positive_qty_done(self):
|
||
|
if any([ml.qty_done < 0 for ml in self]):
|
||
|
raise ValidationError(_('You can not enter negative quantities!'))
|
||
|
|
||
|
@api.constrains('lot_id', 'lot_name', 'qty_done')
|
||
|
def _check_unique_serial_number(self):
|
||
|
for ml in self.filtered(lambda ml: ml.move_id.product_id.tracking == 'serial' and (ml.lot_id or ml.lot_name)):
|
||
|
move_lines_to_check = ml._get_similar_move_lines()
|
||
|
message = move_lines_to_check._check_for_duplicated_serial_numbers()
|
||
|
if message:
|
||
|
raise ValidationError(message)
|
||
|
if float_compare(ml.qty_done, 1.0, precision_rounding=ml.move_id.product_id.uom_id.rounding) == 1:
|
||
|
raise UserError(_(
|
||
|
'You can only process 1.0 %s for products with unique serial number.') % ml.product_id.uom_id.name)
|
||
|
if ml.lot_name:
|
||
|
already_exist = self.env['stock.production.lot'].search(
|
||
|
[('name', '=', ml.lot_name), ('product_id', '=', ml.product_id.id)])
|
||
|
if already_exist:
|
||
|
return _('You have already assigned this serial number to this product. Please correct the serial numbers encoded.')
|
||
|
|
||
|
def _get_similar_move_lines(self):
|
||
|
self.ensure_one()
|
||
|
lines = self.env['stock.move.line']
|
||
|
picking_id = self.move_id.picking_id if self.move_id else self.picking_id
|
||
|
if picking_id:
|
||
|
lines |= picking_id.move_line_ids.filtered(lambda ml: ml.product_id == self.product_id and (ml.lot_id or ml.lot_name))
|
||
|
return lines
|
||
|
|
||
|
def _check_for_duplicated_serial_numbers(self):
|
||
|
""" This method is used in _check_unique_serial_number and in onchange_serial_number to check that a same serial number is not used twice amongst the recordset passed.
|
||
|
|
||
|
:return: an error message directed to the user if needed else False
|
||
|
"""
|
||
|
if self.mapped('lot_id'):
|
||
|
lots_map = [(ml.product_id.id, ml.lot_id.name) for ml in self]
|
||
|
recorded_serials_counter = Counter(lots_map)
|
||
|
for (product_id, lot_id), occurrences in recorded_serials_counter.items():
|
||
|
if occurrences > 1 and lot_id is not False:
|
||
|
return _('You cannot use the same serial number twice. Please correct the serial numbers encoded.')
|
||
|
elif self.mapped('lot_name'):
|
||
|
lots_map = [(ml.product_id.id, ml.lot_name) for ml in self]
|
||
|
recorded_serials_counter = Counter(lots_map)
|
||
|
for (product_id, lot_id), occurrences in recorded_serials_counter.items():
|
||
|
if occurrences > 1 and lot_id is not False:
|
||
|
return _('You cannot use the same serial number twice. Please correct the serial numbers encoded.')
|
||
|
return False
|
||
|
|
||
|
@api.model
|
||
|
def create(self, vals):
|
||
|
vals['ordered_qty'] = vals.get('product_uom_qty')
|
||
|
|
||
|
# If the move line is directly create on the picking view.
|
||
|
# If this picking is already done we should generate an
|
||
|
# associated done move.
|
||
|
if 'picking_id' in vals and 'move_id' not in vals:
|
||
|
picking = self.env['stock.picking'].browse(vals['picking_id'])
|
||
|
if picking.state == 'done':
|
||
|
product = self.env['product.product'].browse(vals['product_id'])
|
||
|
new_move = self.env['stock.move'].create({
|
||
|
'name': _('New Move:') + product.display_name,
|
||
|
'product_id': product.id,
|
||
|
'product_uom_qty': 'qty_done' in vals and vals['qty_done'] or 0,
|
||
|
'product_uom': vals['product_uom_id'],
|
||
|
'location_id': 'location_id' in vals and vals['location_id'] or picking.location_id.id,
|
||
|
'location_dest_id': 'location_dest_id' in vals and vals['location_dest_id'] or picking.location_dest_id.id,
|
||
|
'state': 'done',
|
||
|
'additional': True,
|
||
|
'picking_id': picking.id,
|
||
|
})
|
||
|
vals['move_id'] = new_move.id
|
||
|
|
||
|
ml = super(StockMoveLine, self).create(vals)
|
||
|
if ml.state == 'done':
|
||
|
if ml.product_id.type == 'product':
|
||
|
Quant = self.env['stock.quant']
|
||
|
quantity = ml.product_uom_id._compute_quantity(ml.qty_done, ml.move_id.product_id.uom_id,rounding_method='HALF-UP')
|
||
|
in_date = None
|
||
|
available_qty, in_date = Quant._update_available_quantity(ml.product_id, ml.location_id, -quantity, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id)
|
||
|
if available_qty < 0 and ml.lot_id:
|
||
|
# see if we can compensate the negative quants with some untracked quants
|
||
|
untracked_qty = Quant._get_available_quantity(ml.product_id, ml.location_id, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id, strict=True)
|
||
|
if untracked_qty:
|
||
|
taken_from_untracked_qty = min(untracked_qty, abs(quantity))
|
||
|
Quant._update_available_quantity(ml.product_id, ml.location_id, -taken_from_untracked_qty, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id)
|
||
|
Quant._update_available_quantity(ml.product_id, ml.location_id, taken_from_untracked_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id)
|
||
|
Quant._update_available_quantity(ml.product_id, ml.location_dest_id, quantity, lot_id=ml.lot_id, package_id=ml.result_package_id, owner_id=ml.owner_id, in_date=in_date)
|
||
|
next_moves = ml.move_id.move_dest_ids.filtered(lambda move: move.state not in ('done', 'cancel'))
|
||
|
next_moves._do_unreserve()
|
||
|
next_moves._action_assign()
|
||
|
return ml
|
||
|
|
||
|
def write(self, vals):
|
||
|
""" Through the interface, we allow users to change the charateristics of a move line. If a
|
||
|
quantity has been reserved for this move line, we impact the reservation directly to free
|
||
|
the old quants and allocate the new ones.
|
||
|
"""
|
||
|
if self.env.context.get('bypass_reservation_update'):
|
||
|
return super(StockMoveLine, self).write(vals)
|
||
|
|
||
|
Quant = self.env['stock.quant']
|
||
|
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||
|
# We forbid to change the reserved quantity in the interace, but it is needed in the
|
||
|
# case of stock.move's split.
|
||
|
# TODO Move me in the update
|
||
|
if 'product_uom_qty' in vals:
|
||
|
for ml in self.filtered(lambda m: m.state in ('partially_available', 'assigned') and m.product_id.type == 'product'):
|
||
|
if not ml.location_id.should_bypass_reservation():
|
||
|
qty_to_decrease = ml.product_qty - ml.product_uom_id._compute_quantity(vals['product_uom_qty'], ml.product_id.uom_id, rounding_method='HALF-UP')
|
||
|
try:
|
||
|
Quant._update_reserved_quantity(ml.product_id, ml.location_id, -qty_to_decrease, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id, strict=True)
|
||
|
except UserError:
|
||
|
if ml.lot_id:
|
||
|
Quant._update_reserved_quantity(ml.product_id, ml.location_id, -qty_to_decrease, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id, strict=True)
|
||
|
else:
|
||
|
raise
|
||
|
|
||
|
triggers = [
|
||
|
('location_id', 'stock.location'),
|
||
|
('location_dest_id', 'stock.location'),
|
||
|
('lot_id', 'stock.production.lot'),
|
||
|
('package_id', 'stock.quant.package'),
|
||
|
('result_package_id', 'stock.quant.package'),
|
||
|
('owner_id', 'res.partner')
|
||
|
]
|
||
|
updates = {}
|
||
|
for key, model in triggers:
|
||
|
if key in vals:
|
||
|
updates[key] = self.env[model].browse(vals[key])
|
||
|
|
||
|
if updates:
|
||
|
for ml in self.filtered(lambda ml: ml.state in ['partially_available', 'assigned'] and ml.product_id.type == 'product'):
|
||
|
if not ml.location_id.should_bypass_reservation():
|
||
|
try:
|
||
|
Quant._update_reserved_quantity(ml.product_id, ml.location_id, -ml.product_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id, strict=True)
|
||
|
except UserError:
|
||
|
if ml.lot_id:
|
||
|
Quant._update_reserved_quantity(ml.product_id, ml.location_id, -ml.product_qty, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id, strict=True)
|
||
|
else:
|
||
|
raise
|
||
|
|
||
|
if not updates.get('location_id', ml.location_id).should_bypass_reservation():
|
||
|
new_product_qty = 0
|
||
|
try:
|
||
|
q = Quant._update_reserved_quantity(ml.product_id, updates.get('location_id', ml.location_id), ml.product_qty, lot_id=updates.get('lot_id', ml.lot_id),
|
||
|
package_id=updates.get('package_id', ml.package_id), owner_id=updates.get('owner_id', ml.owner_id), strict=True)
|
||
|
new_product_qty = sum([x[1] for x in q])
|
||
|
except UserError:
|
||
|
if updates.get('lot_id'):
|
||
|
# If we were not able to reserve on tracked quants, we can use untracked ones.
|
||
|
try:
|
||
|
q = Quant._update_reserved_quantity(ml.product_id, updates.get('location_id', ml.location_id), ml.product_qty, lot_id=False,
|
||
|
package_id=updates.get('package_id', ml.package_id), owner_id=updates.get('owner_id', ml.owner_id), strict=True)
|
||
|
new_product_qty = sum([x[1] for x in q])
|
||
|
except UserError:
|
||
|
pass
|
||
|
if new_product_qty != ml.product_qty:
|
||
|
new_product_uom_qty = self.product_id.uom_id._compute_quantity(new_product_qty, self.product_uom_id, rounding_method='HALF-UP')
|
||
|
ml.with_context(bypass_reservation_update=True).product_uom_qty = new_product_uom_qty
|
||
|
|
||
|
# When editing a done move line, the reserved availability of a potential chained move is impacted. Take care of running again `_action_assign` on the concerned moves.
|
||
|
next_moves = self.env['stock.move']
|
||
|
if updates or 'qty_done' in vals:
|
||
|
for ml in self.filtered(lambda ml: ml.move_id.state == 'done' and ml.product_id.type == 'product'):
|
||
|
# undo the original move line
|
||
|
qty_done_orig = ml.move_id.product_uom._compute_quantity(ml.qty_done, ml.move_id.product_id.uom_id, rounding_method='HALF-UP')
|
||
|
in_date = Quant._update_available_quantity(ml.product_id, ml.location_dest_id, -qty_done_orig, lot_id=ml.lot_id,
|
||
|
package_id=ml.result_package_id, owner_id=ml.owner_id)[1]
|
||
|
Quant._update_available_quantity(ml.product_id, ml.location_id, qty_done_orig, lot_id=ml.lot_id,
|
||
|
package_id=ml.package_id, owner_id=ml.owner_id, in_date=in_date)
|
||
|
|
||
|
# move what's been actually done
|
||
|
product_id = ml.product_id
|
||
|
location_id = updates.get('location_id', ml.location_id)
|
||
|
location_dest_id = updates.get('location_dest_id', ml.location_dest_id)
|
||
|
qty_done = vals.get('qty_done', ml.qty_done)
|
||
|
lot_id = updates.get('lot_id', ml.lot_id)
|
||
|
package_id = updates.get('package_id', ml.package_id)
|
||
|
result_package_id = updates.get('result_package_id', ml.result_package_id)
|
||
|
owner_id = updates.get('owner_id', ml.owner_id)
|
||
|
quantity = ml.move_id.product_uom._compute_quantity(qty_done, ml.move_id.product_id.uom_id, rounding_method='HALF-UP')
|
||
|
if not location_id.should_bypass_reservation():
|
||
|
ml._free_reservation(product_id, location_id, quantity, lot_id=lot_id, package_id=package_id, owner_id=owner_id)
|
||
|
if not float_is_zero(quantity, precision_digits=precision):
|
||
|
available_qty, in_date = Quant._update_available_quantity(product_id, location_id, -quantity, lot_id=lot_id, package_id=package_id, owner_id=owner_id)
|
||
|
if available_qty < 0 and lot_id:
|
||
|
# see if we can compensate the negative quants with some untracked quants
|
||
|
untracked_qty = Quant._get_available_quantity(product_id, location_id, lot_id=False, package_id=package_id, owner_id=owner_id, strict=True)
|
||
|
if untracked_qty:
|
||
|
taken_from_untracked_qty = min(untracked_qty, abs(available_qty))
|
||
|
Quant._update_available_quantity(product_id, location_id, -taken_from_untracked_qty, lot_id=False, package_id=package_id, owner_id=owner_id)
|
||
|
Quant._update_available_quantity(product_id, location_id, taken_from_untracked_qty, lot_id=lot_id, package_id=package_id, owner_id=owner_id)
|
||
|
if not location_id.should_bypass_reservation():
|
||
|
ml._free_reservation(ml.product_id, location_id, untracked_qty, lot_id=False, package_id=package_id, owner_id=owner_id)
|
||
|
Quant._update_available_quantity(product_id, location_dest_id, quantity, lot_id=lot_id, package_id=result_package_id, owner_id=owner_id, in_date=in_date)
|
||
|
# Unreserve and reserve following move in order to have the real reserved quantity on move_line.
|
||
|
next_moves |= ml.move_id.move_dest_ids.filtered(lambda move: move.state not in ('done', 'cancel'))
|
||
|
|
||
|
# Log a note
|
||
|
if ml.picking_id:
|
||
|
ml._log_message(ml.picking_id, ml, 'stock.track_move_template', vals)
|
||
|
|
||
|
res = super(StockMoveLine, self).write(vals)
|
||
|
|
||
|
# Update scrap object linked to move_lines to the new quantity.
|
||
|
if 'qty_done' in vals:
|
||
|
for move in self.mapped('move_id'):
|
||
|
if move.scrapped:
|
||
|
move.scrap_ids.write({'scrap_qty': move.quantity_done})
|
||
|
|
||
|
# As stock_account values according to a move's `product_uom_qty`, we consider that any
|
||
|
# done stock move should have its `quantity_done` equals to its `product_uom_qty`, and
|
||
|
# this is what move's `action_done` will do. So, we replicate the behavior here.
|
||
|
if updates or 'qty_done' in vals:
|
||
|
moves = self.filtered(lambda ml: ml.move_id.state == 'done').mapped('move_id')
|
||
|
for move in moves:
|
||
|
move.product_uom_qty = move.quantity_done
|
||
|
next_moves._do_unreserve()
|
||
|
next_moves._action_assign()
|
||
|
return res
|
||
|
|
||
|
def unlink(self):
|
||
|
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||
|
for ml in self:
|
||
|
if ml.state in ('done', 'cancel'):
|
||
|
raise UserError(_('You can not delete product moves if the picking is done. You can only correct the done quantities.'))
|
||
|
# Unlinking a move line should unreserve.
|
||
|
if ml.product_id.type == 'product' and not ml.location_id.should_bypass_reservation() and not float_is_zero(ml.product_qty, precision_digits=precision):
|
||
|
self.env['stock.quant']._update_reserved_quantity(ml.product_id, ml.location_id, -ml.product_qty, lot_id=ml.lot_id,
|
||
|
package_id=ml.package_id, owner_id=ml.owner_id, strict=True)
|
||
|
moves = self.mapped('move_id')
|
||
|
res = super(StockMoveLine, self).unlink()
|
||
|
if moves:
|
||
|
moves._recompute_state()
|
||
|
return res
|
||
|
|
||
|
def _action_done(self):
|
||
|
""" This method is called during a move's `action_done`. It'll actually move a quant from
|
||
|
the source location to the destination location, and unreserve if needed in the source
|
||
|
location.
|
||
|
|
||
|
This method is intended to be called on all the move lines of a move. This method is not
|
||
|
intended to be called when editing a `done` move (that's what the override of `write` here
|
||
|
is done.
|
||
|
"""
|
||
|
|
||
|
# First, we loop over all the move lines to do a preliminary check: `qty_done` should not
|
||
|
# be negative and, according to the presence of a picking type or a linked inventory
|
||
|
# adjustment, enforce some rules on the `lot_id` field. If `qty_done` is null, we unlink
|
||
|
# the line. It is mandatory in order to free the reservation and correctly apply
|
||
|
# `action_done` on the next move lines.
|
||
|
ml_to_delete = self.env['stock.move.line']
|
||
|
for ml in self:
|
||
|
qty_done_float_compared = float_compare(ml.qty_done, 0, precision_rounding=ml.product_uom_id.rounding)
|
||
|
if qty_done_float_compared > 0:
|
||
|
if ml.product_id.tracking != 'none':
|
||
|
picking_type_id = ml.move_id.picking_type_id
|
||
|
if picking_type_id:
|
||
|
if picking_type_id.use_create_lots:
|
||
|
# If a picking type is linked, we may have to create a production lot on
|
||
|
# the fly before assigning it to the move line if the user checked both
|
||
|
# `use_create_lots` and `use_existing_lots`.
|
||
|
if ml.lot_name and not ml.lot_id:
|
||
|
lot = self.env['stock.production.lot'].create(
|
||
|
{'name': ml.lot_name, 'product_id': ml.product_id.id}
|
||
|
)
|
||
|
ml.write({'lot_id': lot.id})
|
||
|
elif not picking_type_id.use_create_lots and not picking_type_id.use_existing_lots:
|
||
|
# If the user disabled both `use_create_lots` and `use_existing_lots`
|
||
|
# checkboxes on the picking type, he's allowed to enter tracked
|
||
|
# products without a `lot_id`.
|
||
|
continue
|
||
|
elif ml.move_id.inventory_id:
|
||
|
# If an inventory adjustment is linked, the user is allowed to enter
|
||
|
# tracked products without a `lot_id`.
|
||
|
continue
|
||
|
|
||
|
if not ml.lot_id:
|
||
|
raise UserError(_('You need to supply a lot/serial number for %s.') % ml.product_id.name)
|
||
|
elif qty_done_float_compared < 0:
|
||
|
raise UserError(_('No negative quantities allowed'))
|
||
|
else:
|
||
|
ml_to_delete |= ml
|
||
|
ml_to_delete.unlink()
|
||
|
|
||
|
# Now, we can actually move the quant.
|
||
|
for ml in self - ml_to_delete:
|
||
|
if ml.product_id.type == 'product':
|
||
|
Quant = self.env['stock.quant']
|
||
|
rounding = ml.product_uom_id.rounding
|
||
|
|
||
|
# if this move line is force assigned, unreserve elsewhere if needed
|
||
|
if not ml.location_id.should_bypass_reservation() and float_compare(ml.qty_done, ml.product_qty, precision_rounding=rounding) > 0:
|
||
|
extra_qty = ml.qty_done - ml.product_qty
|
||
|
ml._free_reservation(ml.product_id, ml.location_id, extra_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id)
|
||
|
# unreserve what's been reserved
|
||
|
if not ml.location_id.should_bypass_reservation() and ml.product_id.type == 'product' and ml.product_qty:
|
||
|
try:
|
||
|
Quant._update_reserved_quantity(ml.product_id, ml.location_id, -ml.product_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id, strict=True)
|
||
|
except UserError:
|
||
|
Quant._update_reserved_quantity(ml.product_id, ml.location_id, -ml.product_qty, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id, strict=True)
|
||
|
|
||
|
# move what's been actually done
|
||
|
quantity = ml.product_uom_id._compute_quantity(ml.qty_done, ml.move_id.product_id.uom_id, rounding_method='HALF-UP')
|
||
|
available_qty, in_date = Quant._update_available_quantity(ml.product_id, ml.location_id, -quantity, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id)
|
||
|
if available_qty < 0 and ml.lot_id:
|
||
|
# see if we can compensate the negative quants with some untracked quants
|
||
|
untracked_qty = Quant._get_available_quantity(ml.product_id, ml.location_id, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id, strict=True)
|
||
|
if untracked_qty:
|
||
|
taken_from_untracked_qty = min(untracked_qty, abs(quantity))
|
||
|
Quant._update_available_quantity(ml.product_id, ml.location_id, -taken_from_untracked_qty, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id)
|
||
|
Quant._update_available_quantity(ml.product_id, ml.location_id, taken_from_untracked_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id)
|
||
|
Quant._update_available_quantity(ml.product_id, ml.location_dest_id, quantity, lot_id=ml.lot_id, package_id=ml.result_package_id, owner_id=ml.owner_id, in_date=in_date)
|
||
|
# Reset the reserved quantity as we just moved it to the destination location.
|
||
|
(self - ml_to_delete).with_context(bypass_reservation_update=True).write({'product_uom_qty': 0.00})
|
||
|
|
||
|
def _log_message(self, record, move, template, vals):
|
||
|
data = vals.copy()
|
||
|
if 'lot_id' in vals and vals['lot_id'] != move.lot_id.id:
|
||
|
data['lot_name'] = self.env['stock.production.lot'].browse(vals.get('lot_id')).name
|
||
|
if 'location_id' in vals:
|
||
|
data['location_name'] = self.env['stock.location'].browse(vals.get('location_id')).name
|
||
|
if 'location_dest_id' in vals:
|
||
|
data['location_dest_name'] = self.env['stock.location'].browse(vals.get('location_dest_id')).name
|
||
|
if 'package_id' in vals and vals['package_id'] != move.package_id.id:
|
||
|
data['package_name'] = self.env['stock.quant.package'].browse(vals.get('package_id')).name
|
||
|
if 'package_result_id' in vals and vals['package_result_id'] != move.package_result_id.id:
|
||
|
data['result_package_name'] = self.env['stock.quant.package'].browse(vals.get('result_package_id')).name
|
||
|
if 'owner_id' in vals and vals['owner_id'] != move.owner_id.id:
|
||
|
data['owner_name'] = self.env['res.partner'].browse(vals.get('owner_id')).name
|
||
|
record.message_post_with_view(template, values={'move': move, 'vals': dict(vals, **data)}, subtype_id=self.env.ref('mail.mt_note').id)
|
||
|
|
||
|
def _free_reservation(self, product_id, location_id, quantity, lot_id=None, package_id=None, owner_id=None):
|
||
|
""" When editing a done move line or validating one with some forced quantities, it is
|
||
|
possible to impact quants that were not reserved. It is therefore necessary to edit or
|
||
|
unlink the move lines that reserved a quantity now unavailable.
|
||
|
"""
|
||
|
self.ensure_one()
|
||
|
|
||
|
# Check the available quantity, with the `strict` kw set to `True`. If the available
|
||
|
# quantity is greather than the quantity now unavailable, there is nothing to do.
|
||
|
available_quantity = self.env['stock.quant']._get_available_quantity(
|
||
|
product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=True
|
||
|
)
|
||
|
if quantity > available_quantity:
|
||
|
# We now have to find the move lines that reserved our now unavailable quantity. We
|
||
|
# take care to exclude ourselves and the move lines were work had already been done.
|
||
|
oudated_move_lines_domain = [
|
||
|
('move_id.state', 'not in', ['done', 'cancel']),
|
||
|
('product_id', '=', product_id.id),
|
||
|
('lot_id', '=', lot_id.id if lot_id else False),
|
||
|
('location_id', '=', location_id.id),
|
||
|
('owner_id', '=', owner_id.id if owner_id else False),
|
||
|
('package_id', '=', package_id.id if package_id else False),
|
||
|
('product_qty', '>', 0.0),
|
||
|
('id', '!=', self.id),
|
||
|
]
|
||
|
oudated_candidates = self.env['stock.move.line'].search(oudated_move_lines_domain)
|
||
|
|
||
|
# As the move's state is not computed over the move lines, we'll have to manually
|
||
|
# recompute the moves which we adapted their lines.
|
||
|
move_to_recompute_state = self.env['stock.move']
|
||
|
|
||
|
rounding = self.product_uom_id.rounding
|
||
|
for candidate in oudated_candidates:
|
||
|
if float_compare(candidate.product_qty, quantity, precision_rounding=rounding) <= 0:
|
||
|
quantity -= candidate.product_qty
|
||
|
move_to_recompute_state |= candidate.move_id
|
||
|
if candidate.qty_done:
|
||
|
candidate.product_uom_qty = 0.0
|
||
|
else:
|
||
|
candidate.unlink()
|
||
|
else:
|
||
|
# split this move line and assign the new part to our extra move
|
||
|
quantity_split = float_round(
|
||
|
candidate.product_qty - quantity,
|
||
|
precision_rounding=self.product_uom_id.rounding,
|
||
|
rounding_method='UP')
|
||
|
candidate.product_uom_qty = self.product_id.uom_id._compute_quantity(quantity_split, candidate.product_uom_id, rounding_method='HALF-UP')
|
||
|
quantity -= quantity_split
|
||
|
move_to_recompute_state |= candidate.move_id
|
||
|
if quantity == 0.0:
|
||
|
break
|
||
|
move_to_recompute_state._recompute_state()
|