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 defaultdict
2018-01-16 11:34:37 +01:00
from flectra import api , fields , models , _
from flectra . exceptions import UserError
2018-04-05 10:25:40 +02:00
from flectra . tools import float_compare , float_round , float_is_zero , pycompat
2018-01-16 06:58:15 +01:00
import logging
_logger = logging . getLogger ( __name__ )
class StockInventory ( models . Model ) :
_inherit = " stock.inventory "
accounting_date = fields . Date (
' Force Accounting Date ' ,
help = " Choose the accounting date at which you want to value the stock "
" moves created by the inventory instead of the default one (the "
" inventory end date) " )
@api.multi
def post_inventory ( self ) :
acc_inventories = self . filtered ( lambda inventory : inventory . accounting_date )
for inventory in acc_inventories :
res = super ( StockInventory , inventory . with_context ( force_period_date = inventory . accounting_date ) ) . post_inventory ( )
other_inventories = self - acc_inventories
if other_inventories :
res = super ( StockInventory , other_inventories ) . post_inventory ( )
return res
class StockLocation ( models . Model ) :
_inherit = " stock.location "
valuation_in_account_id = fields . Many2one (
' account.account ' , ' Stock Valuation Account (Incoming) ' ,
domain = [ ( ' internal_type ' , ' = ' , ' other ' ) , ( ' deprecated ' , ' = ' , False ) ] ,
help = " Used for real-time inventory valuation. When set on a virtual location (non internal type), "
" this account will be used to hold the value of products being moved from an internal location "
" into this location, instead of the generic Stock Output Account set on the product. "
" This has no effect for internal locations. " )
valuation_out_account_id = fields . Many2one (
' account.account ' , ' Stock Valuation Account (Outgoing) ' ,
domain = [ ( ' internal_type ' , ' = ' , ' other ' ) , ( ' deprecated ' , ' = ' , False ) ] ,
help = " Used for real-time inventory valuation. When set on a virtual location (non internal type), "
" this account will be used to hold the value of products being moved out of this location "
" and into an internal location, instead of the generic Stock Output Account set on the product. "
" This has no effect for internal locations. " )
def _should_be_valued ( self ) :
""" This method returns a boolean reflecting whether the products stored in `self` should
be considered when valuating the stock of a company .
"""
self . ensure_one ( )
if self . usage == ' internal ' or ( self . usage == ' transit ' and self . company_id ) :
return True
return False
class StockMoveLine ( models . Model ) :
_inherit = ' stock.move.line '
@api.model
def create ( self , vals ) :
res = super ( StockMoveLine , self ) . create ( vals )
move = res . move_id
if move . state == ' done ' :
correction_value = move . _run_valuation ( res . qty_done )
if move . product_id . valuation == ' real_time ' and ( move . _is_in ( ) or move . _is_out ( ) ) :
move . with_context ( force_valuation_amount = correction_value ) . _account_entry_move ( )
return res
2018-07-09 13:42:38 +02:00
2018-01-16 06:58:15 +01:00
@api.multi
def write ( self , vals ) :
2018-07-09 13:42:38 +02:00
""" When editing a done stock.move.line, we impact the valuation. Users may increase or
decrease the ` qty_done ` field . There are three cost method available : standard , average
and fifo . We implement the logic in a similar way for standard and average : increase
or decrease the original value with the standard or average price of today . In fifo , we
have a different logic wheter the move is incoming or outgoing . If the move is incoming , we
update the value and remaining_value / qty with the unit price of the move . If the move is
outgoing and the user increases qty_done , we call _run_fifo and it ' ll consume layer(s) in
the stack the same way a new outgoing move would have done . If the move is outoing and the
user decreases qty_done , we either increase the last receipt candidate if one is found or
we decrease the value with the last fifo price .
"""
2018-01-16 06:58:15 +01:00
if ' qty_done ' in vals :
moves_to_update = { }
for move_line in self . filtered ( lambda ml : ml . state == ' done ' and ( ml . move_id . _is_in ( ) or ml . move_id . _is_out ( ) ) ) :
moves_to_update [ move_line . move_id ] = vals [ ' qty_done ' ] - move_line . qty_done
for move_id , qty_difference in moves_to_update . items ( ) :
2018-07-09 13:42:38 +02:00
move_vals = { }
2018-01-16 06:58:15 +01:00
if move_id . product_id . cost_method in [ ' standard ' , ' average ' ] :
correction_value = qty_difference * move_id . product_id . standard_price
2018-07-09 13:42:38 +02:00
if move_id . _is_in ( ) :
move_vals [ ' value ' ] = move_id . value + correction_value
elif move_id . _is_out ( ) :
move_vals [ ' value ' ] = move_id . value - correction_value
2018-01-16 06:58:15 +01:00
else :
if move_id . _is_in ( ) :
correction_value = qty_difference * move_id . price_unit
new_remaining_value = move_id . remaining_value + correction_value
2018-07-09 13:42:38 +02:00
move_vals [ ' value ' ] = move_id . value + correction_value
move_vals [ ' remaining_qty ' ] = move_id . remaining_qty + qty_difference
move_vals [ ' remaining_value ' ] = move_id . remaining_value + correction_value
2018-01-16 06:58:15 +01:00
elif move_id . _is_out ( ) and qty_difference > 0 :
correction_value = self . env [ ' stock.move ' ] . _run_fifo ( move_id , quantity = qty_difference )
2018-07-09 13:42:38 +02:00
# no need to adapt `remaining_qty` and `remaining_value` as `_run_fifo` took care of it
move_vals [ ' value ' ] = move_id . value - correction_value
2018-01-16 06:58:15 +01:00
elif move_id . _is_out ( ) and qty_difference < 0 :
candidates_receipt = self . env [ ' stock.move ' ] . search ( move_id . _get_in_domain ( ) , order = ' date, id desc ' , limit = 1 )
if candidates_receipt :
candidates_receipt . write ( {
' remaining_qty ' : candidates_receipt . remaining_qty + - qty_difference ,
' remaining_value ' : candidates_receipt . remaining_value + ( - qty_difference * candidates_receipt . price_unit ) ,
} )
correction_value = qty_difference * candidates_receipt . price_unit
else :
correction_value = qty_difference * move_id . product_id . standard_price
2018-07-09 13:42:38 +02:00
move_vals [ ' value ' ] = move_id . value - correction_value
2018-01-16 06:58:15 +01:00
move_id . write ( move_vals )
if move_id . product_id . valuation == ' real_time ' :
2018-07-09 13:42:38 +02:00
move_id . with_context ( force_valuation_amount = correction_value , forced_quantity = qty_difference ) . _account_entry_move ( )
2018-01-16 06:58:15 +01:00
if qty_difference > 0 :
move_id . product_price_update_before_done ( forced_qty = qty_difference )
return super ( StockMoveLine , self ) . write ( vals )
class StockMove ( models . Model ) :
_inherit = " stock.move "
to_refund = fields . Boolean ( string = " To Refund (update SO/PO) " , copy = False ,
help = ' Trigger a decrease of the delivered/received quantity in the associated Sale Order/Purchase Order ' )
value = fields . Float ( copy = False )
remaining_qty = fields . Float ( copy = False )
remaining_value = fields . Float ( copy = False )
account_move_ids = fields . One2many ( ' account.move ' , ' stock_move_id ' )
@api.multi
def action_get_account_moves ( self ) :
self . ensure_one ( )
action_ref = self . env . ref ( ' account.action_move_journal_line ' )
if not action_ref :
return False
action_data = action_ref . read ( ) [ 0 ]
action_data [ ' domain ' ] = [ ( ' id ' , ' in ' , self . account_move_ids . ids ) ]
return action_data
def _get_price_unit ( self ) :
""" Returns the unit price to store on the quant """
2018-07-19 16:21:56 +02:00
return not self . company_id . currency_id . is_zero ( self . price_unit ) and self . price_unit or self . product_id . standard_price
2018-01-16 06:58:15 +01:00
@api.model
def _get_in_base_domain ( self , company_id = False ) :
domain = [
( ' state ' , ' = ' , ' done ' ) ,
( ' location_id.company_id ' , ' = ' , False ) ,
( ' location_dest_id.company_id ' , ' = ' , company_id or self . env . user . company_id . id )
]
return domain
@api.model
def _get_all_base_domain ( self , company_id = False ) :
domain = [
( ' state ' , ' = ' , ' done ' ) ,
' | ' ,
' & ' ,
( ' location_id.company_id ' , ' = ' , False ) ,
( ' location_dest_id.company_id ' , ' = ' , company_id or self . env . user . company_id . id ) ,
' & ' ,
( ' location_id.company_id ' , ' = ' , company_id or self . env . user . company_id . id ) ,
( ' location_dest_id.company_id ' , ' = ' , False )
]
return domain
def _get_in_domain ( self ) :
return [ ( ' product_id ' , ' = ' , self . product_id . id ) ] + self . _get_in_base_domain ( company_id = self . company_id . id )
def _get_all_domain ( self ) :
return [ ( ' product_id ' , ' = ' , self . product_id . id ) ] + self . _get_all_base_domain ( company_id = self . company_id . id )
def _is_in ( self ) :
""" Check if the move should be considered as entering the company so that the cost method
will be able to apply the correct logic .
: return : True if the move is entering the company else False
"""
for move_line in self . move_line_ids . filtered ( lambda ml : not ml . owner_id ) :
if not move_line . location_id . _should_be_valued ( ) and move_line . location_dest_id . _should_be_valued ( ) :
return True
return False
def _is_out ( self ) :
""" Check if the move should be considered as leaving the company so that the cost method
will be able to apply the correct logic .
: return : True if the move is leaving the company else False
"""
for move_line in self . move_line_ids . filtered ( lambda ml : not ml . owner_id ) :
if move_line . location_id . _should_be_valued ( ) and not move_line . location_dest_id . _should_be_valued ( ) :
return True
return False
2018-04-05 10:25:40 +02:00
def _is_dropshipped ( self ) :
""" Check if the move should be considered as a dropshipping move so that the cost method
will be able to apply the correct logic .
: return : True if the move is a dropshipping one else False
"""
return self . location_id . usage == ' supplier ' and self . location_dest_id . usage == ' customer '
2018-01-16 06:58:15 +01:00
@api.model
def _run_fifo ( self , move , quantity = None ) :
2018-07-09 13:42:38 +02:00
""" Value `move` according to the FIFO rule, meaning we consume the
oldest receipt first . Candidates receipts are marked consumed or free
thanks to their ` remaining_qty ` and ` remaining_value ` fields .
By definition , ` move ` should be an outgoing stock move .
: param quantity : quantity to value instead of ` move . product_qty `
: returns : valued amount in absolute
"""
2018-01-16 06:58:15 +01:00
move . ensure_one ( )
2018-07-09 13:42:38 +02:00
# Deal with possible move lines that do not impact the valuation.
2018-01-16 06:58:15 +01:00
valued_move_lines = move . move_line_ids . filtered ( lambda ml : ml . location_id . _should_be_valued ( ) and not ml . location_dest_id . _should_be_valued ( ) and not ml . owner_id )
valued_quantity = 0
for valued_move_line in valued_move_lines :
valued_quantity + = valued_move_line . product_uom_id . _compute_quantity ( valued_move_line . qty_done , move . product_id . uom_id )
2018-07-09 13:42:38 +02:00
# Find back incoming stock moves (called candidates here) to value this move.
2018-01-16 06:58:15 +01:00
qty_to_take_on_candidates = quantity or valued_quantity
candidates = move . product_id . _get_fifo_candidates_in_move ( )
new_standard_price = 0
tmp_value = 0 # to accumulate the value taken on the candidates
for candidate in candidates :
new_standard_price = candidate . price_unit
if candidate . remaining_qty < = qty_to_take_on_candidates :
qty_taken_on_candidate = candidate . remaining_qty
else :
qty_taken_on_candidate = qty_to_take_on_candidates
# As applying a landed cost do not update the unit price, naivelly doing
# something like qty_taken_on_candidate * candidate.price_unit won't make
# the additional value brought by the landed cost go away.
candidate_price_unit = candidate . remaining_value / candidate . remaining_qty
value_taken_on_candidate = qty_taken_on_candidate * candidate_price_unit
candidate_vals = {
' remaining_qty ' : candidate . remaining_qty - qty_taken_on_candidate ,
' remaining_value ' : candidate . remaining_value - value_taken_on_candidate ,
}
candidate . write ( candidate_vals )
qty_to_take_on_candidates - = qty_taken_on_candidate
tmp_value + = value_taken_on_candidate
if qty_to_take_on_candidates == 0 :
break
# Update the standard price with the price of the last used candidate, if any.
if new_standard_price and move . product_id . cost_method == ' fifo ' :
2018-07-09 13:42:38 +02:00
move . product_id . sudo ( ) . standard_price = new_standard_price
2018-01-16 06:58:15 +01:00
# If there's still quantity to value but we're out of candidates, we fall in the
# negative stock use case. We chose to value the out move at the price of the
# last out and a correction entry will be made once `_fifo_vacuum` is called.
if qty_to_take_on_candidates == 0 :
move . write ( {
' value ' : - tmp_value if not quantity else move . value or - tmp_value , # outgoing move are valued negatively
' price_unit ' : - tmp_value / move . product_qty ,
} )
elif qty_to_take_on_candidates > 0 :
last_fifo_price = new_standard_price or move . product_id . standard_price
negative_stock_value = last_fifo_price * - qty_to_take_on_candidates
2018-07-09 13:42:38 +02:00
tmp_value + = abs ( negative_stock_value )
2018-01-16 06:58:15 +01:00
vals = {
' remaining_qty ' : move . remaining_qty + - qty_to_take_on_candidates ,
' remaining_value ' : move . remaining_value + negative_stock_value ,
2018-07-09 13:42:38 +02:00
' value ' : - tmp_value ,
' price_unit ' : - 1 * last_fifo_price ,
2018-01-16 06:58:15 +01:00
}
move . write ( vals )
return tmp_value
def _run_valuation ( self , quantity = None ) :
self . ensure_one ( )
if self . _is_in ( ) :
valued_move_lines = self . move_line_ids . filtered ( lambda ml : not ml . location_id . _should_be_valued ( ) and ml . location_dest_id . _should_be_valued ( ) and not ml . owner_id )
valued_quantity = 0
for valued_move_line in valued_move_lines :
valued_quantity + = valued_move_line . product_uom_id . _compute_quantity ( valued_move_line . qty_done , self . product_id . uom_id )
# Note: we always compute the fifo `remaining_value` and `remaining_qty` fields no
# matter which cost method is set, to ease the switching of cost method.
vals = { }
price_unit = self . _get_price_unit ( )
value = price_unit * ( quantity or valued_quantity )
vals = {
' price_unit ' : price_unit ,
' value ' : value if quantity is None or not self . value else self . value ,
' remaining_value ' : value if quantity is None else self . remaining_value + value ,
}
vals [ ' remaining_qty ' ] = valued_quantity if quantity is None else self . remaining_qty + quantity
if self . product_id . cost_method == ' standard ' :
value = self . product_id . standard_price * ( quantity or valued_quantity )
vals . update ( {
' price_unit ' : self . product_id . standard_price ,
' value ' : value if quantity is None or not self . value else self . value ,
} )
self . write ( vals )
elif self . _is_out ( ) :
valued_move_lines = self . move_line_ids . filtered ( lambda ml : ml . location_id . _should_be_valued ( ) and not ml . location_dest_id . _should_be_valued ( ) and not ml . owner_id )
2018-07-09 13:42:38 +02:00
valued_quantity = 0
for valued_move_line in valued_move_lines :
valued_quantity + = valued_move_line . product_uom_id . _compute_quantity ( valued_move_line . qty_done , self . product_id . uom_id )
2018-01-16 06:58:15 +01:00
self . env [ ' stock.move ' ] . _run_fifo ( self , quantity = quantity )
if self . product_id . cost_method in [ ' standard ' , ' average ' ] :
curr_rounding = self . company_id . currency_id . rounding
value = - float_round ( self . product_id . standard_price * ( valued_quantity if quantity is None else quantity ) , precision_rounding = curr_rounding )
self . write ( {
' value ' : value if quantity is None else self . value + value ,
' price_unit ' : value / valued_quantity ,
} )
2018-04-05 10:25:40 +02:00
elif self . _is_dropshipped ( ) :
curr_rounding = self . company_id . currency_id . rounding
if self . product_id . cost_method in [ ' fifo ' ] :
price_unit = self . _get_price_unit ( )
# see test_dropship_fifo_perpetual_anglosaxon_ordered
self . product_id . standard_price = price_unit
else :
price_unit = self . product_id . standard_price
value = float_round ( self . product_qty * price_unit , precision_rounding = curr_rounding )
# In move have a positive value, out move have a negative value, let's arbitrary say
# dropship are positive.
self . write ( {
' value ' : value ,
' price_unit ' : price_unit ,
} )
2018-01-16 06:58:15 +01:00
def _action_done ( self ) :
self . product_price_update_before_done ( )
res = super ( StockMove , self ) . _action_done ( )
for move in res :
# Apply restrictions on the stock move to be able to make
# consistent accounting entries.
if move . _is_in ( ) and move . _is_out ( ) :
raise UserError ( _ ( " The move lines are not in a consistent state: some are entering and other are leaving the company. " ) )
company_src = move . mapped ( ' move_line_ids.location_id.company_id ' )
company_dst = move . mapped ( ' move_line_ids.location_dest_id.company_id ' )
try :
if company_src :
company_src . ensure_one ( )
if company_dst :
company_dst . ensure_one ( )
except ValueError :
raise UserError ( _ ( " The move lines are not in a consistent states: they do not share the same origin or destination company. " ) )
if company_src and company_dst and company_src . id != company_dst . id :
raise UserError ( _ ( " The move lines are not in a consistent states: they are doing an intercompany in a single step while they should go through the intercompany transit location. " ) )
move . _run_valuation ( )
2018-04-05 10:25:40 +02:00
for move in res . filtered ( lambda m : m . product_id . valuation == ' real_time ' and ( m . _is_in ( ) or m . _is_out ( ) or m . _is_dropshipped ( ) ) ) :
2018-01-16 06:58:15 +01:00
move . _account_entry_move ( )
return res
@api.multi
def product_price_update_before_done ( self , forced_qty = None ) :
tmpl_dict = defaultdict ( lambda : 0.0 )
# adapt standard price on incomming moves if the product cost_method is 'average'
std_price_update = { }
for move in self . filtered ( lambda move : move . location_id . usage in ( ' supplier ' , ' production ' ) and move . product_id . cost_method == ' average ' ) :
product_tot_qty_available = move . product_id . qty_available + tmpl_dict [ move . product_id . id ]
2018-04-05 10:25:40 +02:00
rounding = move . product_id . uom_id . rounding
2018-01-16 06:58:15 +01:00
2018-07-09 13:42:38 +02:00
qty_done = 0.0
2018-04-05 10:25:40 +02:00
if float_is_zero ( product_tot_qty_available , precision_rounding = rounding ) :
new_std_price = move . _get_price_unit ( )
elif float_is_zero ( product_tot_qty_available + move . product_qty , precision_rounding = rounding ) :
2018-01-16 06:58:15 +01:00
new_std_price = move . _get_price_unit ( )
else :
# Get the standard price
amount_unit = std_price_update . get ( ( move . company_id . id , move . product_id . id ) ) or move . product_id . standard_price
2018-07-09 13:42:38 +02:00
qty_done = move . product_uom . _compute_quantity ( move . quantity_done , move . product_id . uom_id )
qty = forced_qty or qty_done
new_std_price = ( ( amount_unit * product_tot_qty_available ) + ( move . _get_price_unit ( ) * qty ) ) / ( product_tot_qty_available + qty_done )
2018-01-16 06:58:15 +01:00
2018-07-09 13:42:38 +02:00
tmpl_dict [ move . product_id . id ] + = qty_done
2018-01-16 06:58:15 +01:00
# Write the standard price, as SUPERUSER_ID because a warehouse manager may not have the right to write on products
move . product_id . with_context ( force_company = move . company_id . id ) . sudo ( ) . write ( { ' standard_price ' : new_std_price } )
std_price_update [ move . company_id . id , move . product_id . id ] = new_std_price
@api.model
def _fifo_vacuum ( self ) :
""" Every moves that need to be fixed are identifiable by having a negative `remaining_qty`.
"""
for move in self . filtered ( lambda m : ( m . _is_in ( ) or m . _is_out ( ) ) and m . remaining_qty < 0 ) :
domain = [
( ' remaining_qty ' , ' > ' , 0 ) ,
' | ' ,
( ' date ' , ' > ' , move . date ) ,
' & ' ,
( ' date ' , ' = ' , move . date ) ,
( ' id ' , ' > ' , move . id )
]
domain + = move . _get_in_domain ( )
candidates = self . search ( domain , order = ' date, id ' )
if not candidates :
continue
qty_to_take_on_candidates = abs ( move . remaining_qty )
2018-07-09 13:42:38 +02:00
qty_taken_on_candidates = 0
2018-01-16 06:58:15 +01:00
tmp_value = 0
for candidate in candidates :
if candidate . remaining_qty < = qty_to_take_on_candidates :
qty_taken_on_candidate = candidate . remaining_qty
else :
qty_taken_on_candidate = qty_to_take_on_candidates
2018-07-09 13:42:38 +02:00
qty_taken_on_candidates + = qty_taken_on_candidate
2018-01-16 06:58:15 +01:00
value_taken_on_candidate = qty_taken_on_candidate * candidate . price_unit
candidate_vals = {
' remaining_qty ' : candidate . remaining_qty - qty_taken_on_candidate ,
' remaining_value ' : candidate . remaining_value - value_taken_on_candidate ,
}
candidate . write ( candidate_vals )
qty_to_take_on_candidates - = qty_taken_on_candidate
tmp_value + = value_taken_on_candidate
if qty_to_take_on_candidates == 0 :
break
2018-07-09 13:42:38 +02:00
# When working with `price_unit`, beware that out move are negative.
move_price_unit = move . price_unit if move . _is_out ( ) else - 1 * move . price_unit
# Get the estimated value we will correct.
remaining_value_before_vacuum = qty_taken_on_candidates * move_price_unit
new_remaining_qty = move . remaining_qty + qty_taken_on_candidates
new_remaining_value = new_remaining_qty * abs ( move . price_unit )
corrected_value = remaining_value_before_vacuum + tmp_value
2018-01-16 06:58:15 +01:00
move . write ( {
' remaining_value ' : new_remaining_value ,
' remaining_qty ' : new_remaining_qty ,
2018-07-09 13:42:38 +02:00
' value ' : move . value - corrected_value ,
2018-01-16 06:58:15 +01:00
} )
if move . product_id . valuation == ' real_time ' :
2018-04-05 10:25:40 +02:00
# If `corrected_value` is 0, absolutely do *not* call `_account_entry_move`. We
# force the amount in the context, but in the case it is 0 it'll create an entry
# for the entire cost of the move. This case happens when the candidates moves
# entirely compensate the problematic move.
if not corrected_value :
continue
2018-01-16 06:58:15 +01:00
if move . _is_in ( ) :
# If we just compensated an IN move that has a negative remaining
# quantity, it means the move has returned more items than it received.
# The correction should behave as a return too. As `_account_entry_move`
# will post the natural values for an IN move (credit IN account, debit
# OUT one), we inverse the sign to create the correct entries.
2018-07-09 13:42:38 +02:00
move . with_context ( force_valuation_amount = - corrected_value , forced_quantity = 0 ) . _account_entry_move ( )
2018-01-16 06:58:15 +01:00
else :
2018-07-09 13:42:38 +02:00
move . with_context ( force_valuation_amount = corrected_value , forced_quantity = 0 ) . _account_entry_move ( )
2018-01-16 06:58:15 +01:00
@api.model
def _run_fifo_vacuum ( self ) :
# Call `_fifo_vacuum` on concerned moves
fifo_valued_products = self . env [ ' product.product ' ]
fifo_valued_products | = self . env [ ' product.template ' ] . search ( [ ( ' property_cost_method ' , ' = ' , ' fifo ' ) ] ) . mapped (
' product_variant_ids ' )
fifo_valued_categories = self . env [ ' product.category ' ] . search ( [ ( ' property_cost_method ' , ' = ' , ' fifo ' ) ] )
fifo_valued_products | = self . env [ ' product.product ' ] . search ( [ ( ' categ_id ' , ' child_of ' , fifo_valued_categories . ids ) ] )
moves_to_vacuum = self . env [ ' stock.move ' ]
for product in fifo_valued_products :
moves_to_vacuum | = self . search (
[ ( ' product_id ' , ' = ' , product . id ) , ( ' remaining_qty ' , ' < ' , 0 ) ] + self . _get_all_base_domain ( ) )
moves_to_vacuum . _fifo_vacuum ( )
@api.multi
def _get_accounting_data_for_valuation ( self ) :
""" Return the accounts and journal to use to post Journal Entries for
the real - time valuation of the quant . """
self . ensure_one ( )
accounts_data = self . product_id . product_tmpl_id . get_product_accounts ( )
if self . location_id . valuation_out_account_id :
acc_src = self . location_id . valuation_out_account_id . id
else :
acc_src = accounts_data [ ' stock_input ' ] . id
if self . location_dest_id . valuation_in_account_id :
acc_dest = self . location_dest_id . valuation_in_account_id . id
else :
acc_dest = accounts_data [ ' stock_output ' ] . id
acc_valuation = accounts_data . get ( ' stock_valuation ' , False )
if acc_valuation :
acc_valuation = acc_valuation . id
if not accounts_data . get ( ' stock_journal ' , False ) :
raise UserError ( _ ( ' You don \' t have any stock journal defined on your product category, check if you have installed a chart of accounts ' ) )
if not acc_src :
raise UserError ( _ ( ' Cannot find a stock input account for the product %s . You must define one on the product category, or on the location, before processing this operation. ' ) % ( self . product_id . name ) )
if not acc_dest :
raise UserError ( _ ( ' Cannot find a stock output account for the product %s . You must define one on the product category, or on the location, before processing this operation. ' ) % ( self . product_id . name ) )
if not acc_valuation :
raise UserError ( _ ( ' You don \' t have any stock valuation account defined on your product category. You must define one before processing this operation. ' ) )
journal_id = accounts_data [ ' stock_journal ' ] . id
return journal_id , acc_src , acc_dest , acc_valuation
def _prepare_account_move_line ( self , qty , cost , credit_account_id , debit_account_id ) :
"""
Generate the account . move . line values to post to track the stock valuation difference due to the
processing of the given quant .
"""
self . ensure_one ( )
if self . _context . get ( ' force_valuation_amount ' ) :
valuation_amount = self . _context . get ( ' force_valuation_amount ' )
else :
valuation_amount = cost
2018-07-09 13:42:38 +02:00
if self . _context . get ( ' forced_ref ' ) :
ref = self . _context [ ' forced_ref ' ]
else :
ref = self . picking_id . name
2018-01-16 06:58:15 +01:00
# the standard_price of the product may be in another decimal precision, or not compatible with the coinage of
# the company currency... so we need to use round() before creating the accounting entries.
debit_value = self . company_id . currency_id . round ( valuation_amount )
# check that all data is correct
if self . company_id . currency_id . is_zero ( debit_value ) :
raise UserError ( _ ( " The cost of %s is currently equal to 0. Change the cost or the configuration of your product to avoid an incorrect valuation. " ) % ( self . product_id . name , ) )
credit_value = debit_value
partner_id = ( self . picking_id . partner_id and self . env [ ' res.partner ' ] . _find_accounting_partner ( self . picking_id . partner_id ) . id ) or False
debit_line_vals = {
' name ' : self . name ,
' product_id ' : self . product_id . id ,
' quantity ' : qty ,
' product_uom_id ' : self . product_id . uom_id . id ,
2018-07-09 13:42:38 +02:00
' ref ' : ref ,
2018-01-16 06:58:15 +01:00
' partner_id ' : partner_id ,
' debit ' : debit_value if debit_value > 0 else 0 ,
' credit ' : - debit_value if debit_value < 0 else 0 ,
' account_id ' : debit_account_id ,
}
credit_line_vals = {
' name ' : self . name ,
' product_id ' : self . product_id . id ,
' quantity ' : qty ,
' product_uom_id ' : self . product_id . uom_id . id ,
2018-07-09 13:42:38 +02:00
' ref ' : ref ,
2018-01-16 06:58:15 +01:00
' partner_id ' : partner_id ,
' credit ' : credit_value if credit_value > 0 else 0 ,
' debit ' : - credit_value if credit_value < 0 else 0 ,
' account_id ' : credit_account_id ,
}
res = [ ( 0 , 0 , debit_line_vals ) , ( 0 , 0 , credit_line_vals ) ]
if credit_value != debit_value :
# for supplier returns of product in average costing method, in anglo saxon mode
diff_amount = debit_value - credit_value
price_diff_account = self . product_id . property_account_creditor_price_difference
if not price_diff_account :
price_diff_account = self . product_id . categ_id . property_account_creditor_price_difference_categ
if not price_diff_account :
raise UserError ( _ ( ' Configuration error. Please configure the price difference account on the product or its category to process this operation. ' ) )
price_diff_line = {
' name ' : self . name ,
' product_id ' : self . product_id . id ,
' quantity ' : qty ,
' product_uom_id ' : self . product_id . uom_id . id ,
2018-07-09 13:42:38 +02:00
' ref ' : ref ,
2018-01-16 06:58:15 +01:00
' partner_id ' : partner_id ,
' credit ' : diff_amount > 0 and diff_amount or 0 ,
' debit ' : diff_amount < 0 and - diff_amount or 0 ,
' account_id ' : price_diff_account . id ,
}
res . append ( ( 0 , 0 , price_diff_line ) )
return res
def _create_account_move_line ( self , credit_account_id , debit_account_id , journal_id ) :
self . ensure_one ( )
AccountMove = self . env [ ' account.move ' ]
2018-07-09 13:42:38 +02:00
quantity = self . env . context . get ( ' forced_quantity ' , self . product_qty )
quantity = quantity if self . _is_in ( ) else - 1 * quantity
# Make an informative `ref` on the created account move to differentiate between classic
# movements, vacuum and edition of past moves.
ref = self . picking_id . name
if self . env . context . get ( ' force_valuation_amount ' ) :
if self . env . context . get ( ' forced_quantity ' ) == 0 :
ref = ' Revaluation of %s (negative inventory) ' % ref
elif self . env . context . get ( ' forced_quantity ' ) is not None :
ref = ' Correction of %s (modification of past move) ' % ref
move_lines = self . with_context ( forced_ref = ref ) . _prepare_account_move_line ( quantity , abs ( self . value ) , credit_account_id , debit_account_id )
2018-01-16 06:58:15 +01:00
if move_lines :
date = self . _context . get ( ' force_period_date ' , fields . Date . context_today ( self ) )
2018-07-09 13:42:38 +02:00
new_account_move = AccountMove . sudo ( ) . create ( {
2018-01-16 06:58:15 +01:00
' journal_id ' : journal_id ,
' line_ids ' : move_lines ,
' date ' : date ,
2018-07-09 13:42:38 +02:00
' ref ' : ref ,
2018-01-16 06:58:15 +01:00
' stock_move_id ' : self . id ,
} )
new_account_move . post ( )
def _account_entry_move ( self ) :
""" Accounting Valuation Entries """
self . ensure_one ( )
if self . product_id . type != ' product ' :
# no stock valuation for consumable products
return False
if self . restrict_partner_id :
# if the move isn't owned by the company, we don't make any valuation
return False
location_from = self . location_id
location_to = self . location_dest_id
company_from = self . _is_out ( ) and self . mapped ( ' move_line_ids.location_id.company_id ' ) or False
company_to = self . _is_in ( ) and self . mapped ( ' move_line_ids.location_dest_id.company_id ' ) or False
# Create Journal Entry for products arriving in the company; in case of routes making the link between several
# warehouse of the same company, the transit location belongs to this company, so we don't need to create accounting entries
if self . _is_in ( ) :
journal_id , acc_src , acc_dest , acc_valuation = self . _get_accounting_data_for_valuation ( )
if location_from and location_from . usage == ' customer ' : # goods returned from customer
self . with_context ( force_company = company_to . id ) . _create_account_move_line ( acc_dest , acc_valuation , journal_id )
else :
self . with_context ( force_company = company_to . id ) . _create_account_move_line ( acc_src , acc_valuation , journal_id )
# Create Journal Entry for products leaving the company
if self . _is_out ( ) :
journal_id , acc_src , acc_dest , acc_valuation = self . _get_accounting_data_for_valuation ( )
if location_to and location_to . usage == ' supplier ' : # goods returned to supplier
self . with_context ( force_company = company_from . id ) . _create_account_move_line ( acc_valuation , acc_src , journal_id )
else :
self . with_context ( force_company = company_from . id ) . _create_account_move_line ( acc_valuation , acc_dest , journal_id )
2018-04-05 10:25:40 +02:00
if self . company_id . anglo_saxon_accounting and self . _is_dropshipped ( ) :
# Creates an account entry from stock_input to stock_output on a dropship move. https://github.com/odoo/odoo/issues/12687
2018-01-16 06:58:15 +01:00
journal_id , acc_src , acc_dest , acc_valuation = self . _get_accounting_data_for_valuation ( )
self . with_context ( force_company = self . company_id . id ) . _create_account_move_line ( acc_src , acc_dest , journal_id )
class StockReturnPicking ( models . TransientModel ) :
_inherit = " stock.return.picking "
@api.multi
def _create_returns ( self ) :
new_picking_id , pick_type_id = super ( StockReturnPicking , self ) . _create_returns ( )
new_picking = self . env [ ' stock.picking ' ] . browse ( [ new_picking_id ] )
for move in new_picking . move_lines :
return_picking_line = self . product_return_moves . filtered ( lambda r : r . move_id == move . origin_returned_move_id )
if return_picking_line and return_picking_line . to_refund :
move . to_refund = True
return new_picking_id , pick_type_id
class StockReturnPickingLine ( models . TransientModel ) :
_inherit = " stock.return.picking.line "
to_refund = fields . Boolean ( string = " To Refund (update SO/PO) " , help = ' Trigger a decrease of the delivered/received quantity in the associated Sale Order/Purchase Order ' )
class ProcurementGroup ( models . Model ) :
_inherit = ' procurement.group '
@api.model
def _run_scheduler_tasks ( self , use_new_cursor = False , company_id = False ) :
super ( ProcurementGroup , self ) . _run_scheduler_tasks ( use_new_cursor = use_new_cursor , company_id = company_id )
self . env [ ' stock.move ' ] . _run_fifo_vacuum ( )