2013-07-11 13:05:28 +00:00
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import time
from openerp . osv import fields , osv
from openerp . tools . translate import _
import logging
_logger = logging . getLogger ( __name__ )
#----------------------------------------------------------
# Stock Location
#----------------------------------------------------------
class stock_location ( osv . osv ) :
_inherit = " stock.location "
_columns = {
2013-09-09 10:02:29 +00:00
' valuation_in_account_id ' : fields . many2one ( ' account.account ' , ' Stock Valuation Account (Incoming) ' , domain = [ ( ' type ' , ' = ' , ' other ' ) ] ,
2013-07-11 13:05:28 +00:00
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. " ) ,
2013-09-09 10:02:29 +00:00
' valuation_out_account_id ' : fields . many2one ( ' account.account ' , ' Stock Valuation Account (Outgoing) ' , domain = [ ( ' type ' , ' = ' , ' other ' ) ] ,
2013-07-11 13:05:28 +00:00
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. " ) ,
}
#----------------------------------------------------------
# Quants
#----------------------------------------------------------
class stock_quant ( osv . osv ) :
_inherit = " stock.quant "
2013-07-22 16:15:33 +00:00
def _get_inventory_value ( self , cr , uid , line , prodbrow , context = None ) :
#TODO: what in case of partner_id
if prodbrow [ ( line . company_id . id , line . product_id . id ) ] . cost_method in ( ' real ' ) :
2013-07-23 09:44:36 +00:00
return line . cost * line . qty
2013-07-22 16:15:33 +00:00
return super ( stock_quant , self ) . _get_inventory_value ( cr , uid , line , prodbrow , context = context )
2013-07-11 13:05:28 +00:00
# FP Note: this is where we should post accounting entries for adjustment
def _price_update ( self , cr , uid , quant , newprice , context = None ) :
super ( stock_quant , self ) . _price_update ( cr , uid , quant , newprice , context = context )
# TODO: generate accounting entries
"""
Accounting Valuation Entries
location_from : can be None if it ' s a new quant
"""
def _account_entry_move ( self , cr , uid , quant , location_from , location_to , move , context = None ) :
2013-07-16 08:06:34 +00:00
if context is None :
context = { }
2013-07-17 13:26:27 +00:00
if quant . product_id . valuation != ' real_time ' :
return False
2013-08-02 09:04:46 +00:00
if quant . lot_id and quant . lot_id . partner_id :
#if the quant isn't owned by the company, we don't make any valuation entry
return False
2013-07-17 13:26:27 +00:00
if quant . qty < = 0 or quant . propagated_from_id :
#we don't make any stock valuation for negative quants because we may not know the real cost price.
#The valuation will be made at the time of the reconciliation of the negative quant.
2013-07-11 13:05:28 +00:00
return False
company_from = self . _location_owner ( cr , uid , quant , location_from , context = context )
company_to = self . _location_owner ( cr , uid , quant , location_to , context = context )
if company_from == company_to :
return False
# Create Journal Entry for products arriving in the company
if company_to :
2013-07-16 08:06:34 +00:00
ctx = context . copy ( )
ctx [ ' force_company ' ] = company_to . id
journal_id , acc_src , acc_dest , acc_valuation = self . _get_accounting_data_for_valuation ( cr , uid , move , context = ctx )
2013-07-15 11:22:19 +00:00
if location_from and location_from . usage == ' customer ' :
2013-07-12 14:14:07 +00:00
#goods returned from customer
2013-07-16 08:06:34 +00:00
self . _create_account_move_line ( cr , uid , quant , move , acc_dest , acc_valuation , journal_id , context = ctx )
2013-07-12 14:14:07 +00:00
else :
2013-07-16 08:06:34 +00:00
self . _create_account_move_line ( cr , uid , quant , move , acc_src , acc_valuation , journal_id , context = ctx )
2013-07-11 13:05:28 +00:00
# Create Journal Entry for products leaving the company
if company_from :
2013-07-16 08:06:34 +00:00
ctx = context . copy ( )
ctx [ ' force_company ' ] = company_from . id
journal_id , acc_src , acc_dest , acc_valuation = self . _get_accounting_data_for_valuation ( cr , uid , move , context = ctx )
2013-07-15 11:22:19 +00:00
if location_to and location_to . usage == ' supplier ' :
2013-07-12 14:14:07 +00:00
#goods returned to supplier
2013-07-16 08:06:34 +00:00
self . _create_account_move_line ( cr , uid , quant , move , acc_valuation , acc_src , journal_id , context = ctx )
2013-07-12 14:14:07 +00:00
else :
2013-07-16 08:06:34 +00:00
self . _create_account_move_line ( cr , uid , quant , move , acc_valuation , acc_dest , journal_id , context = ctx )
2013-07-11 13:05:28 +00:00
def move_single_quant ( self , cr , uid , quant , qty , move , context = None ) :
2013-07-15 11:22:19 +00:00
location_from = quant and quant . location_id or False
2013-07-15 09:30:53 +00:00
quant = super ( stock_quant , self ) . move_single_quant ( cr , uid , quant , qty , move , context = context )
2013-07-12 14:14:07 +00:00
quant . refresh ( )
self . _account_entry_move ( cr , uid , quant , location_from , quant . location_id , move , context = context )
2013-07-15 09:30:53 +00:00
return quant
2013-07-11 13:05:28 +00:00
2013-07-12 14:14:07 +00:00
def _get_accounting_data_for_valuation ( self , cr , uid , move , context = None ) :
"""
Return the accounts and journal to use to post Journal Entries for the real - time
valuation of the quant .
2013-07-11 13:05:28 +00:00
2013-07-12 14:14:07 +00:00
: param context : context dictionary that can explicitly mention the company to consider via the ' force_company ' key
: returns : journal_id , source account , destination account , valuation account
: raise : osv . except_osv ( ) is any mandatory account or journal is not defined .
"""
2013-07-17 13:26:27 +00:00
product_obj = self . pool . get ( ' product.product ' )
2013-07-12 14:14:07 +00:00
accounts = product_obj . get_product_accounts ( cr , uid , move . product_id . id , context )
if move . location_id . valuation_out_account_id :
acc_src = move . location_id . valuation_out_account_id . id
else :
acc_src = accounts [ ' stock_account_input ' ]
2013-07-11 13:05:28 +00:00
2013-07-12 14:14:07 +00:00
if move . location_dest_id . valuation_in_account_id :
acc_dest = move . location_dest_id . valuation_in_account_id . id
else :
acc_dest = accounts [ ' stock_account_output ' ]
2013-07-11 13:05:28 +00:00
2013-07-12 14:14:07 +00:00
acc_valuation = accounts . get ( ' property_stock_valuation_account_id ' , False )
journal_id = accounts [ ' stock_journal ' ]
2013-07-11 13:05:28 +00:00
2013-07-12 14:14:07 +00:00
if not all ( [ acc_src , acc_dest , acc_valuation , journal_id ] ) :
2013-07-17 13:26:27 +00:00
raise osv . except_osv ( _ ( ' Error! ' ) , _ ( ''' One of the following information is missing on the product or product category and prevents the accounting valuation entries to be created:
2013-07-12 14:14:07 +00:00
Stock Input Account : % s
Stock Output Account : % s
Stock Valuation Account : % s
Stock Journal : % s
''' ) % (acc_src, acc_dest, acc_valuation, journal_id))
return journal_id , acc_src , acc_dest , acc_valuation
2013-07-11 13:05:28 +00:00
2013-07-16 08:06:34 +00:00
def _prepare_account_move_line ( self , cr , uid , quant , move , credit_account_id , debit_account_id , context = None ) :
2013-07-12 14:14:07 +00:00
"""
Generate the account . move . line values to post to track the stock valuation difference due to the
processing of the given quant .
"""
valuation_amount = quant . product_id . cost_method == ' real ' and quant . cost or quant . product_id . standard_price
partner_id = ( move . picking_id . partner_id and self . pool . get ( ' res.partner ' ) . _find_accounting_partner ( move . picking_id . partner_id ) . id ) or False
debit_line_vals = {
' name ' : move . name ,
' product_id ' : quant . product_id . id ,
2013-07-15 11:22:19 +00:00
' quantity ' : quant . qty ,
2013-09-09 10:02:29 +00:00
' product_uom_id ' : quant . product_id . uom_id . id ,
2013-07-12 14:14:07 +00:00
' ref ' : move . picking_id and move . picking_id . name or False ,
' date ' : time . strftime ( ' % Y- % m- %d ' ) ,
' partner_id ' : partner_id ,
2013-07-16 08:06:34 +00:00
' debit ' : valuation_amount * quant . qty ,
2013-07-12 14:14:07 +00:00
' account_id ' : debit_account_id ,
}
credit_line_vals = {
' name ' : move . name ,
' product_id ' : quant . product_id . id ,
2013-07-15 11:22:19 +00:00
' quantity ' : quant . qty ,
2013-09-09 10:02:29 +00:00
' product_uom_id ' : quant . product_id . uom_id . id ,
2013-07-12 14:14:07 +00:00
' ref ' : move . picking_id and move . picking_id . name or False ,
' date ' : time . strftime ( ' % Y- % m- %d ' ) ,
' partner_id ' : partner_id ,
2013-07-16 08:06:34 +00:00
' credit ' : valuation_amount * quant . qty ,
2013-07-12 14:14:07 +00:00
' account_id ' : credit_account_id ,
}
2013-07-15 08:18:09 +00:00
res = [ ( 0 , 0 , debit_line_vals ) , ( 0 , 0 , credit_line_vals ) ]
2013-07-12 14:14:07 +00:00
return res
2013-07-11 13:05:28 +00:00
2013-07-16 08:06:34 +00:00
def _create_account_move_line ( self , cr , uid , quant , move , credit_account_id , debit_account_id , journal_id , context = None ) :
move_obj = self . pool . get ( ' account.move ' )
move_lines = self . _prepare_account_move_line ( cr , uid , quant , move , credit_account_id , debit_account_id , context = context )
return move_obj . create ( cr , uid , { ' journal_id ' : journal_id ,
' line_id ' : move_lines ,
' ref ' : move . picking_id and move . picking_id . name } , context = context )
2013-09-09 10:02:29 +00:00
def _reconcile_single_negative_quant ( self , cr , uid , to_solve_quant , quant , quant_neg , qty , context = None ) :
move = self . _get_latest_move ( cr , uid , to_solve_quant , context = context )
quant_neg_position = quant_neg . negative_dest_location_id . usage
2013-09-18 13:04:58 +00:00
remaining_solving_quant , remaining_to_solve_quant = super ( stock_quant , self ) . _reconcile_single_negative_quant ( cr , uid , to_solve_quant , quant , quant_neg , qty , context = context )
2013-09-09 10:02:29 +00:00
#update the standard price of the product, only if we would have done it if we'd have had enough stock at first, which means
#1) there isn't any negative quant anymore
#2) the product cost's method is 'real'
#3) we just fixed a negative quant caused by an outgoing shipment
if not remaining_to_solve_quant and move . product_id . cost_method == ' real ' and quant_neg_position != ' internal ' :
self . pool . get ( ' stock.move ' ) . _store_average_cost_price ( cr , uid , move , context = context )
2013-09-18 13:04:58 +00:00
return remaining_solving_quant , remaining_to_solve_quant
2013-09-09 10:02:29 +00:00
2013-09-03 16:27:57 +00:00
class stock_move ( osv . osv ) :
_inherit = " stock.move "
def action_done ( self , cr , uid , ids , context = None ) :
2013-09-09 15:47:54 +00:00
self . product_price_update_before_done ( cr , uid , ids , context = context )
2013-09-09 11:31:01 +00:00
super ( stock_move , self ) . action_done ( cr , uid , ids , context = context )
2013-09-09 15:47:54 +00:00
self . product_price_update_after_done ( cr , uid , ids , context = context )
2013-07-16 08:06:34 +00:00
2013-09-09 10:02:29 +00:00
def _store_average_cost_price ( self , cr , uid , move , context = None ) :
''' move is a browe record '''
product_obj = self . pool . get ( ' product.product ' )
2013-09-09 15:47:54 +00:00
move . refresh ( )
2013-09-09 10:02:29 +00:00
if any ( [ q . qty < = 0 for q in move . quant_ids ] ) :
#if there is a negative quant, the standard price shouldn't be updated
return
#Note: here we can't store a quant.cost directly as we may have moved out 2 units (1 unit to 5€ and 1 unit to 7€) and in case of a product return of 1 unit, we can't know which of the 2 costs has to be used (5€ or 7€?). So at that time, thanks to the average valuation price we are storing we will svaluate it at 6€
average_valuation_price = 0.0
for q in move . quant_ids :
average_valuation_price + = q . qty * q . cost
average_valuation_price = average_valuation_price / move . product_qty
product_obj . write ( cr , uid , move . product_id . id , { ' standard_price ' : average_valuation_price } , context = context )
self . write ( cr , uid , move . id , { ' price_unit ' : average_valuation_price } , context = context )
2013-09-09 15:47:54 +00:00
def product_price_update_before_done ( self , cr , uid , ids , context = None ) :
2013-09-03 16:27:57 +00:00
product_obj = self . pool . get ( ' product.product ' )
for move in self . browse ( cr , uid , ids , context = context ) :
2013-09-05 10:34:35 +00:00
#adapt standard price on incomming moves if the product cost_method is 'average'
if ( move . location_id . usage == ' supplier ' ) and ( move . product_id . cost_method == ' average ' ) :
product = move . product_id
company_currency_id = move . company_id . currency_id . id
ctx = { ' currency_id ' : company_currency_id }
product_avail = product . qty_available
if product . qty_available < = 0 :
new_std_price = move . price_unit
else :
# Get the standard price
amount_unit = product . price_get ( ' standard_price ' , context = ctx ) [ product . id ]
2013-09-09 15:47:54 +00:00
new_std_price = ( ( amount_unit * product_avail ) + ( move . price_unit * move . product_qty ) ) / ( product_avail + move . product_qty )
2013-09-05 10:34:35 +00:00
# Write the field according to price type field
product_obj . write ( cr , uid , [ product . id ] , { ' standard_price ' : new_std_price } , context = context )
2013-09-09 15:47:54 +00:00
def product_price_update_after_done ( self , cr , uid , ids , context = None ) :
'''
This method adapts the price on the product when necessary
'''
for move in self . browse ( cr , uid , ids , context = context ) :
2013-09-05 10:34:35 +00:00
#adapt standard price on outgoing moves if the product cost_method is 'real', so that a return
#or an inventory loss is made using the last value used for an outgoing valuation.
2013-09-03 16:27:57 +00:00
if move . product_id . cost_method == ' real ' and move . location_dest_id . usage != ' internal ' :
2013-09-09 10:02:29 +00:00
#store the average price of the move on the move and product form
self . _store_average_cost_price ( cr , uid , move , context = context )