2018-01-16 06:58:15 +01:00
# -*- coding: utf-8 -*-
2018-01-16 11:34:37 +01:00
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
2018-01-16 06:58:15 +01:00
from collections import Counter
2018-01-16 11:34:37 +01:00
from flectra import api , fields , models , _
from flectra . addons import decimal_precision as dp
from flectra . exceptions import UserError , ValidationError
from flectra . tools . pycompat import izip
from flectra . tools . float_utils import float_round , float_compare , float_is_zero
2018-01-16 06:58:15 +01:00
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 ' )
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 :
2018-04-05 10:25:40 +02:00
if move_line . state == ' done ' and not float_is_zero ( move_line . product_uom_qty , precision_digits = self . env [ ' decimal.precision ' ] . precision_get ( ' Product Unit of Measure ' ) ) :
2018-01-16 06:58:15 +01:00
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 ) :
2018-04-05 10:25:40 +02:00
""" When the user is encoding a move line for a tracked product, we apply some logic to
help him . This includes :
- automatically switch ` qty_done ` to 1.0
- warn if he has already encoded ` lot_name ` in another move line
"""
2018-01-16 06:58:15 +01:00
res = { }
if self . product_id . tracking == ' serial ' :
2018-04-05 10:25:40 +02:00
if not self . qty_done :
self . qty_done = 1
message = None
if self . lot_name or self . lot_id :
move_lines_to_check = self . _get_similar_move_lines ( ) - self
if self . lot_name :
counter = Counter ( move_lines_to_check . mapped ( ' lot_name ' ) )
if counter . get ( self . lot_name ) and counter [ self . lot_name ] > 1 :
message = _ ( ' You cannot use the same serial number twice. Please correct the serial numbers encoded. ' )
elif self . lot_id :
counter = Counter ( move_lines_to_check . mapped ( ' lot_id.id ' ) )
if counter . get ( self . lot_id . id ) and counter [ self . lot_id . id ] > 1 :
message = _ ( ' You cannot use the same serial number twice. Please correct the serial numbers encoded. ' )
2018-01-16 06:58:15 +01:00
if message :
res [ ' warning ' ] = { ' title ' : _ ( ' Warning ' ) , ' message ' : message }
return res
2018-04-05 10:25:40 +02:00
@api.onchange ( ' qty_done ' )
def _onchange_qty_done ( self ) :
""" When the user is encoding a move line for a tracked product, we apply some logic to
help him . This onchange will warn him if he set ` qty_done ` to a non - supported value .
"""
res = { }
2018-07-06 14:58:06 +02:00
if self . qty_done and self . product_id . tracking == ' serial ' :
2018-04-05 10:25:40 +02:00
if float_compare ( self . qty_done , 1.0 , precision_rounding = self . move_id . product_id . uom_id . rounding ) != 0 :
message = _ ( ' You can only process 1.0 %s for products with unique serial number. ' ) % self . product_id . uom_id . name
res [ ' warning ' ] = { ' title ' : _ ( ' Warning ' ) , ' message ' : message }
return res
2018-01-16 06:58:15 +01:00
@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! ' ) )
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
@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.
2018-04-05 10:25:40 +02:00
if ' picking_id ' in vals and not vals . get ( ' move_id ' ) :
2018-01-16 06:58:15 +01:00
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 :
2018-07-06 14:58:06 +02:00
new_product_uom_qty = ml . product_id . uom_id . _compute_quantity ( new_product_qty , ml . product_uom_id , rounding_method = ' HALF-UP ' )
2018-01-16 06:58:15 +01:00
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 ) :
2018-04-05 10:25:40 +02:00
try :
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 )
except UserError :
if ml . lot_id :
self . env [ ' stock.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
2018-01-16 06:58:15 +01:00
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 :
2018-07-06 14:58:06 +02:00
# Check here if `ml.qty_done` respects the rounding of `ml.product_uom_id`.
uom_qty = float_round ( ml . qty_done , precision_rounding = ml . product_uom_id . rounding , rounding_method = ' HALF-UP ' )
precision_digits = self . env [ ' decimal.precision ' ] . precision_get ( ' Product Unit of Measure ' )
qty_done = float_round ( ml . qty_done , precision_digits = precision_digits , rounding_method = ' HALF-UP ' )
if float_compare ( uom_qty , qty_done , precision_digits = precision_digits ) != 0 :
raise UserError ( _ ( ' The quantity done for the product " %s " doesn \' t respect the rounding precision \
defined on the unit of measure " %s " . Please change the quantity done or the \
rounding precision of your unit of measure . ' ) % (ml.product_id.display_name, ml.product_uom_id.name))
2018-01-16 06:58:15 +01:00
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.
2018-04-05 10:25:40 +02:00
done_ml = self . env [ ' stock.move.line ' ]
2018-01-16 06:58:15 +01:00
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
2018-04-05 10:25:40 +02:00
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 , ml_to_ignore = done_ml )
2018-01-16 06:58:15 +01:00
# 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 )
2018-04-05 10:25:40 +02:00
done_ml | = ml
2018-01-16 06:58:15 +01:00
# Reset the reserved quantity as we just moved it to the destination location.
2018-04-05 10:25:40 +02:00
( self - ml_to_delete ) . with_context ( bypass_reservation_update = True ) . write ( {
' product_uom_qty ' : 0.00 ,
' date ' : fields . Datetime . now ( ) ,
} )
2018-01-16 06:58:15 +01: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 )
2018-04-05 10:25:40 +02:00
def _free_reservation ( self , product_id , location_id , quantity , lot_id = None , package_id = None , owner_id = None , ml_to_ignore = None ) :
2018-01-16 06:58:15 +01:00
""" 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 .
2018-04-05 10:25:40 +02:00
: param ml_to_ignore : recordset of ` stock . move . line ` that should NOT be unreserved
2018-01-16 06:58:15 +01:00
"""
self . ensure_one ( )
2018-04-05 10:25:40 +02:00
if ml_to_ignore is None :
ml_to_ignore = self . env [ ' stock.move.line ' ]
ml_to_ignore | = self
2018-01-16 06:58:15 +01:00
# 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 ) ,
2018-04-05 10:25:40 +02:00
( ' id ' , ' not in ' , ml_to_ignore . ids ) ,
2018-01-16 06:58:15 +01:00
]
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 ( )