203 lines
11 KiB
Python
203 lines
11 KiB
Python
# -*- 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/>.
|
|
#
|
|
##############################################################################
|
|
|
|
from openerp import tools
|
|
from openerp.osv import osv, fields
|
|
|
|
|
|
#@TODO Should not this be product template?
|
|
class product_product (osv.osv):
|
|
_name = "product.product"
|
|
_inherit = "product.product"
|
|
_columns = {
|
|
'cost_method': fields.property('', type='selection', view_load=True, selection = [('standard','Standard Price'), ('average','Average Price'),
|
|
('fifo', 'FIFO price'), ('lifo', 'LIFO price')],
|
|
help="""Standard Price: The cost price is manually updated at the end of a specific period (usually every year).
|
|
Average Price: The cost price is recomputed at each incoming shipment.
|
|
FIFO: When cost is calculated the FIFO way.
|
|
LIFO: When cost is calculated the LIFO way. """,
|
|
string="Costing Method"),
|
|
}
|
|
|
|
|
|
def get_stock_matchings_fifolifo(self, cr, uid, ids, qty, fifo, product_uom_id = False, currency_id = False, context=None):
|
|
'''
|
|
This method returns a list of tuples with quantities from stock in moves
|
|
These are the quantities that would go out theoretically according to the fifo or lifo method if qty needs to go out
|
|
(move_in_id, qty in uom of move out, price (converted to move out), qty in uom of move in
|
|
This should be called for only one product at a time
|
|
UoMs and currencies from the corresponding moves are converted towards that given in the params
|
|
'''
|
|
assert len(ids) == 1, _('Only the fifolifo stock matchings of one product can be calculated at a time.')
|
|
uom_obj = self.pool.get('product.uom')
|
|
currency_obj = self.pool.get('res.currency')
|
|
|
|
product = self.browse(cr, uid, ids, context=context)[0]
|
|
move_obj = self.pool.get('stock.move')
|
|
|
|
if not product_uom_id:
|
|
product_uom_id = product.uom_id.id
|
|
|
|
if 'force_company' in context:
|
|
company_id = context['force_company']
|
|
else:
|
|
#Would be better not to have to go here
|
|
company_id = product.company_id.id
|
|
|
|
if not currency_id:
|
|
currency_id = self.pool.get('res.company').browse(cr, uid, company_id, context=context).currency_id.id
|
|
|
|
if fifo:
|
|
move_in_ids = move_obj.search(cr, uid, [('company_id','=', company_id), ('qty_remaining', '>', 0), ('state', '=', 'done'),
|
|
('type', '=', 'in'), ('product_id', '=', product.id)],
|
|
order = 'date', context=context)
|
|
else:
|
|
move_in_ids = move_obj.search(cr, uid, [('company_id','=', company_id), ('qty_remaining', '>', 0), ('state', '=', 'done'),
|
|
('type', '=', 'in'), ('product_id', '=', product.id)],
|
|
order = 'date desc', context=context)
|
|
tuples = []
|
|
qty_to_go = qty
|
|
for move in move_obj.browse(cr, uid, move_in_ids, context=context):
|
|
#Convert to UoM of product each time
|
|
uom_from = move.product_uom.id
|
|
qty_from = move.qty_remaining
|
|
product_qty = uom_obj._compute_qty(cr, uid, uom_from, qty_from, product_uom_id)
|
|
#Convert currency from in move currency id to out move currency
|
|
if move.price_currency_id and (move.price_currency_id.id != currency_id):
|
|
new_price = currency_obj.compute(cr, uid, move.price_currency_id.id, currency_id,
|
|
move.price_unit, round=False)
|
|
else:
|
|
new_price = move.price_unit
|
|
new_price = uom_obj._compute_price(cr, uid, uom_from, new_price,
|
|
product_uom_id)
|
|
if qty_to_go - product_qty >= 0:
|
|
tuples.append((move.id, product_qty, new_price, qty_from),)
|
|
qty_to_go -= product_qty
|
|
else:
|
|
tuples.append((move.id, qty_to_go, new_price, qty_from * qty_to_go / product_qty),)
|
|
break
|
|
return tuples
|
|
|
|
class stock_move(osv.osv):
|
|
_inherit = 'stock.move'
|
|
_columns = {'qty_remaining': fields.float("Remaining"),
|
|
'matching_ids_in': fields.one2many('stock.move.matching', 'move_in_id'),
|
|
'matching_ids_out':fields.one2many('stock.move.matching', 'move_out_id'),
|
|
}
|
|
|
|
def create(self, cr, uid, vals, context=None):
|
|
if 'product_qty' in vals:
|
|
vals['qty_remaining'] = vals['product_qty']
|
|
res = super(stock_move, self).create(cr, uid, vals, context=context)
|
|
return res
|
|
|
|
def write(self, cr, uid, ids, vals, context=None):
|
|
if 'product_qty' in vals:
|
|
vals['qty_remaining'] = vals['product_qty']
|
|
res = super(stock_move, self).write(cr, uid, ids, vals, context=context)
|
|
return res
|
|
|
|
|
|
#Overwrites method for FIFO computation
|
|
def price_computation(self, cr, uid, ids, partial_datas, context=None):
|
|
super(stock_move, self).price_computation(cr, uid, ids, partial_datas, context=context)
|
|
product_obj = self.pool.get('product.product')
|
|
matching_obj = self.pool.get('stock.move.matching')
|
|
currency_obj = self.pool.get('res.currency')
|
|
uom_obj = self.pool.get('product.uom')
|
|
|
|
#Find stock moves working in fifo/lifo price -> find stock moves out
|
|
for move in self.browse(cr, uid, ids, context=context):
|
|
partial_data = partial_datas.get('move%s'%(move.id), {})
|
|
product_qty = partial_data.get('product_qty',0.0)
|
|
product_uom = partial_data.get('product_uom',False)
|
|
product_price = partial_data.get('product_price',0.0)
|
|
product_currency = partial_data.get('product_currency',False)
|
|
product = product_obj.browse(cr, uid, move.product_id.id, context=context)
|
|
|
|
#Check we are using the right company
|
|
company_id = move.company_id.id
|
|
ctx = context.copy()
|
|
user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
|
|
if company_id != user.company_id.id:
|
|
ctx['force_company'] = company_id
|
|
product = product_obj.browse(cr, uid, move.product_id.id, context=ctx)
|
|
cost_method = product.cost_method
|
|
|
|
if move.picking_id.type == 'out' and cost_method in ['fifo', 'lifo']:
|
|
#get_stock_matchings will convert to currency and UoM of this stock move
|
|
tuples = product_obj.get_stock_matchings_fifolifo(cr, uid, [product.id], product_qty, cost_method == 'fifo',
|
|
product_uom, move.company_id.currency_id.id, context=context) #Always move of the company
|
|
price_amount = 0.0
|
|
amount = 0.0
|
|
move_currency_id = move.company_id.currency_id.id
|
|
ctx['currency_id'] = move_currency_id
|
|
for match in tuples:
|
|
matchvals = {'move_in_id': match[0], 'qty': match[1],
|
|
'move_out_id': move.id}
|
|
match_id = matching_obj.create(cr, uid, matchvals, context=context)
|
|
move_in = self.browse(cr, uid, match[0], context=context)
|
|
#Reduce remaining quantity
|
|
self.write(cr, uid, match[0], { 'qty_remaining': move_in.qty_remaining - match[3]}, context=context)
|
|
price_amount += match[1] * match[2]
|
|
amount += match[1]
|
|
self.write(cr, uid, move.id, {'price_unit': price_amount / amount}, context=context)
|
|
print price_amount
|
|
print amount
|
|
#This price has to be put as the new standard price for the product, but needs to be converted to product UoM and currency
|
|
#convert uom of qty
|
|
uom_id = product.uom_id.id
|
|
product_qty = uom_obj._compute_qty(cr, uid, product_uom, amount, uom_id)
|
|
#convert price, no need of UoM conversion as it is the total price
|
|
currency_id = move.company_id.currency_id.id
|
|
currency_from = move.price_currency_id
|
|
if move.price_currency_id and (move.price_currency_id.id != currency_id):
|
|
new_price = currency_obj.compute(cr, uid, move.price_currency_id, currency_id,
|
|
price_amount)
|
|
else:
|
|
new_price = price_amount
|
|
#new_price does not depend on qty as it is the total amount => no conversion needed for uom
|
|
product_obj.write(cr, uid, product.id, {'standard_price': new_price / product_qty}, context=ctx)
|
|
# When the move is products returned to supplier or return products from customer
|
|
# then the price should be the price from the original move
|
|
elif cost_method in ['fifo', 'lifo']:
|
|
#The currency in the stock move should be the currency of the company
|
|
if product_currency != move.company_id.currency_id.id:
|
|
new_price = currency_obj.compute(cr, uid, product_currency, move.company_id.currency_id.id,
|
|
product_price, round=False)
|
|
else:
|
|
new_price = product_price
|
|
self.write(cr, uid, [move.id],
|
|
{'price_unit': new_price,
|
|
'price_currency_id': move.company_id.currency_id.id})
|
|
return True
|
|
|
|
class stock_move_matching(osv.osv):
|
|
_name = "stock.move.matching"
|
|
_description = "Stock move matchings"
|
|
_columns = {
|
|
'move_in_id': fields.many2one('stock.move', 'Stock move in', required=True),
|
|
'move_out_id': fields.many2one('stock.move', 'Stock move out', required=True),
|
|
'qty': fields.integer('Quantity', required=True),
|
|
'price_unit':fields.related('move_in_id', 'price_unit', string="Unit price", type="float"),
|
|
}
|
|
|