[ADD] Add stock fifo lifo module, add average tests and improve average calc

bzr revid: jco@openerp.com-20130426111730-pf96a6nh2wv42kwl
This commit is contained in:
Josse Colpaert 2013-04-26 13:17:30 +02:00
parent 042e47c5d4
commit c2c8c476e5
10 changed files with 574 additions and 12 deletions

View File

@ -77,6 +77,7 @@ Dashboard / Reports for Purchase Management will include:
'test/ui/print_report.yml',
'test/ui/duplicate_order.yml',
'test/ui/delete_order.yml',
'test/average_price.yml',
],
'demo': [
'purchase_order_demo.yml',

View File

@ -0,0 +1,106 @@
-
Set a product as using average price
-
!record {model: product.product, id: product_average_icecream}:
default_code: AVG
name: Average Ice Cream
type: product
categ_id: product.product_category_1
list_price: 100.0
standard_price: 70.0
uom_id: product.product_uom_kgm
uom_po_id: product.product_uom_kgm
procure_method: make_to_stock
valuation: real_time
cost_method: average
property_stock_account_input: account.o_expense
property_stock_account_output: account.o_income
description: Average Ice Cream can be mass-produced and thus is widely available in developed parts of the world. Ice cream can be purchased in large cartons (vats and squrounds) from supermarkets and grocery stores, in smaller quantities from ice cream shops, convenience stores, and milk bars, and in individual servings from small carts or vans at public events.
-
I create a draft Purchase Order for first in move
-
!record {model: purchase.order, id: purchase_order_average1}:
partner_id: base.res_partner_3
location_id: stock.stock_location_stock
pricelist_id: 1
order_line:
- product_id: product_average_icecream
product_qty: 10.0
product_uom: product.product_uom_categ_kgm
price_unit: 60.0
name: 'Average Ice Cream'
-
I create a draft Purchase Order for second shipment for 30 pieces at 80 euro
-
!record {model: purchase.order, id: purchase_order_average2}:
partner_id: base.res_partner_3
location_id: stock.stock_location_stock
pricelist_id: 1
order_line:
- product_id: product_average_icecream
product_qty: 30.0
product_uom: product.product_uom_categ_kgm
price_unit: 80.0
name: 'Average Ice Cream'
-
I confirm the first purchase order
-
!workflow {model: purchase.order, action: purchase_confirm, ref: purchase_order_average1}
-
I confirm the second purchase order
-
!workflow {model: purchase.order, action: purchase_confirm, ref: purchase_order_average2}
-
I check the "Approved" status of purchase order 1
-
!assert {model: purchase.order, id: purchase_order_average1}:
- state == 'approved'
-
Process the reception of purchase order 1
-
!python {model: stock.partial.picking}: |
pick_ids = self.pool.get('purchase.order').browse(cr, uid, ref("purchase_order_average1")).picking_ids
print pick_ids
partial_id = self.create(cr, uid, {}, context={'active_model': 'stock.picking','active_ids': [pick_ids[0].id]})
self.do_partial(cr, uid, [partial_id])
-
Check the standard price of the product (average icecream)
-
!python {model: product.product}: |
print self.browse(cr, uid, ref("product_average_icecream")).qty_available
assert self.browse(cr, uid, ref("product_average_icecream")).standard_price == 60.0, 'Standard price should not change while receiving products!'
-
Process the reception of purchase order 2
-
!python {model: stock.partial.picking}: |
pick_ids = self.pool.get('purchase.order').browse(cr, uid, ref("purchase_order_average2")).picking_ids
partial_id = self.create(cr, uid, {}, context={'active_model': 'stock.picking','active_ids': [pick_ids[0].id]})
self.do_partial(cr, uid, [partial_id])
-
Check the standard price
-
!python {model: product.product}: |
assert self.browse(cr, uid, ref("product_average_icecream")).standard_price == 75.0, 'Standard price should not change while receiving products!'
-
Let us send some goods
-
!record {model: stock.picking, id: outgoing_average_shipment}:
type: out
-
Picking needs movement from stock
-
!record {model: stock.move, id: outgoing_shipment_average_icecream}:
picking_id: outgoing_average_shipment
product_id: product_average_icecream
product_uom: product.product_uom_kgm
product_qty: 20.0
-
I confirm outgoing shipment of 20 kg of Average Ice Cream. @TODO need to send the pieces still!
-
!workflow {model: stock.picking, action: button_confirm, ref: outgoing_average_shipment}
-
Check the standard price (60 * 10 + 30 * 80) / 40 = 75.0 did not change
-
!python {model: product.product}: |
assert self.browse(cr, uid, ref("product_average_icecream")).standard_price == 75.0, 'Standard price as average price of second reception incorrect!'

View File

@ -2610,11 +2610,12 @@ class stock_move(osv.osv):
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)
company_id = move.company_id.id
if company_id != user.company_id.id:
#test if I could do this with a read instead
ctx['force_company'] = move.company_id.id
product = product_obj.browse(cr, uid, move.product_id.id, context=ctx)
cost_method = product.cost_method
@ -2622,14 +2623,19 @@ class stock_move(osv.osv):
#Check if cost_method used needs update
avg_in_update = (move.picking_id.type == 'in') and (cost_method == 'average')
avg_out_update = (move.picking_id.type == 'out') and (cost_method == 'average') and (move.move_returned_from)
if avg_in_update or avg_out_update:
if avg_in_update or avg_out_update:
# If no price from picking, use cost price from product
if product_price == 0:
product_price = product.price_get('standard_price', context=ctx)[product.id]
move_currency_id = move.company_id.currency_id.id
ctx['currency_id'] = move_currency_id
qty = uom_obj._compute_qty(cr, uid, product_uom, product_qty, product.uom_id.id)
first_avail = False
if not product.id in product_avail:
product_avail[product.id] = product.qty_available
first_avail = True
if qty > 0:
if avg_in_update:
new_price = currency_obj.compute(cr, uid, product_currency,
@ -2652,15 +2658,13 @@ class stock_move(osv.osv):
amount_unit = product.price_get('standard_price', context=ctx)[product.id]
new_std_price = ((amount_unit * product_avail[product.id])\
- (new_price * qty))/(product_avail[product.id] - qty)
# Write the field according to price type field
product_obj.write(cr, uid, [product.id], {'standard_price': new_std_price})
#Adjust product available with the right amount
if not first_avail:
if avg_out_update:
product_avail[product.id] -= qty
else:
product_avail[product.id] += qty
if avg_out_update:
product_avail[product.id] -= qty
else:
product_avail[product.id] += qty
# Write the field according to price type field, company dependence in ctx
product_obj.write(cr, uid, [product.id], {'standard_price': new_std_price}, context=ctx)

View File

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2013 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 stock_fifo_lifo import *
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -0,0 +1,51 @@
# -*- 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/>.
#
##############################################################################
{
'name': 'FIFO/LIFO stock valuation',
'version': '0.1',
'author': 'OpenERP SA',
'summary': 'Valorize your stock FIFO/LIFO',
'description' : """
Manage FIFO/LIFO stock valuation
================================
This gives reports which value the stock in a FIFO/LIFO way. It adds a table to match the outs with the ins.
""",
'website': 'http://www.openerp.com',
'images': [],
'depends': ['purchase'],
'category': 'Warehouse Management',
'sequence': 16,
'demo': [
],
'data': ['security/ir.model.access.csv'
],
'test': ['test/fifolifo_price.yml',
'test/lifo_price.yml'
],
'installable': True,
'application': True,
'auto_install': False,
'css': [],
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -0,0 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_stock_move_matching_manager,stock.move.matching manager,model_stock_move_matching,stock.group_stock_manager,1,1,1,1
access_stock_move_matching_user,stock.move.matching user,model_stock_move_matching,stock.group_stock_user,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_stock_move_matching_manager stock.move.matching manager model_stock_move_matching stock.group_stock_manager 1 1 1 1
3 access_stock_move_matching_user stock.move.matching user model_stock_move_matching stock.group_stock_user 1 1 1 0

View File

@ -0,0 +1,14 @@
<openerp>
<data noupdate="1">
<!-- multi -->
<record model="ir.rule" id="stock_matching_rule">
<field name="name">stock_move matching</field>
<field name="model_id" search="[('model','=','stock.move.matching')]" model="ir.model"/>
<field name="global" eval="True"/>
<field name="domain_force">[('1','=','1')]</field>
</record>
</data>
</openerp>

View File

@ -0,0 +1,139 @@
# -*- 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, context=None):
'''
This method returns a list of tuples for which the stock moves are working fifo/lifo
This should be called for only one product at a time
-> might still need to add UoM
'''
assert len(ids) == 1, 'Only the fifolifo stock moves of one product can be calculated at a time.'
product = self.browse(cr, uid, ids, context=context)[0]
move_obj = self.pool.get('stock.move')
if fifo:
move_ids = move_obj.search(cr, uid, [('qty_remaining', '>', 0), ('state', '=', 'done'),
('type', '=', 'in'), ('product_id', '=', product.id)],
order = 'date', context=context)
else:
move_ids = move_obj.search(cr, uid, [('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_ids, context=context):
# @TODO convert UoM for product quantities?
product_qty = move.product_qty
if qty_to_go - product_qty >= 0:
tuples.append((move.id, product_qty, move.price_unit),)
qty_to_go -= product_qty
else:
tuples.append((move.id, qty_to_go, move.price_unit),)
qty_to_go = 0
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
#@TODO overwrite method for price_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')
#Find stock moves working in fifo/lifo price -> find stock moves out
for move in self.browse(cr, uid, ids, context=context):
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'] = move.company_id.id
product = product_obj.browse(cr, uid, move.product_id.id, context=ctx)
cost_method = product.cost_method
product_price = move.price_unit
#Still need to see how to handle UoM + check quantity from the partial datas
product_qty = move.product_qty
if move.picking_id.type == 'out' and cost_method in ['fifo', 'lifo']:
if not move.move_returned_from:
tuples = product_obj.get_stock_matchings_fifolifo(cr, uid, [product.id], product_qty, cost_method == 'fifo', context=context)
for match in tuples:
matchvals = {'move_in_id': match[0], 'qty': match[1], 'price_unit': match[2],
'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)
self.write(cr, uid, match[0], { 'qty_remaining': move_in.qty_remaining - match[1]}, context=context)
else:
#We should find something to do when a stock matching is linked to the returned move already
if move.move_returned_from.matching_ids_in:
pass
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"),
}

View File

@ -0,0 +1,110 @@
-
Set a product as using fifo price
-
!record {model: product.product, id: product_fifo_icecream}:
default_code: FIFO
name: FIFO Ice Cream
type: product
categ_id: product.product_category_1
list_price: 100.0
standard_price: 70.0
uom_id: product.product_uom_kgm
uom_po_id: product.product_uom_kgm
procure_method: make_to_stock
valuation: real_time
cost_method: fifo
property_stock_account_input: account.o_expense
property_stock_account_output: account.o_income
description: FIFO Ice Cream can be mass-produced and thus is widely available in developed parts of the world. Ice cream can be purchased in large cartons (vats and squrounds) from supermarkets and grocery stores, in smaller quantities from ice cream shops, convenience stores, and milk bars, and in individual servings from small carts or vans at public events.
-
I create a draft Purchase Order for first in move for 10 pieces at 60 euro
-
!record {model: purchase.order, id: purchase_order_fifo1}:
partner_id: base.res_partner_3
location_id: stock.stock_location_stock
pricelist_id: 1
order_line:
- product_id: product_fifo_icecream
product_qty: 10.0
product_uom: product.product_uom_categ_kgm
price_unit: 60.0
name: 'FIFO Ice Cream'
-
I create a draft Purchase Order for second shipment for 30 pieces at 80 euro
-
!record {model: purchase.order, id: purchase_order_fifo2}:
partner_id: base.res_partner_3
location_id: stock.stock_location_stock
pricelist_id: 1
order_line:
- product_id: product_fifo_icecream
product_qty: 30.0
product_uom: product.product_uom_categ_kgm
price_unit: 80.0
name: 'FIFO Ice Cream'
-
I confirm the first purchase order
-
!workflow {model: purchase.order, action: purchase_confirm, ref: purchase_order_fifo1}
-
I confirm the second purchase order
-
!workflow {model: purchase.order, action: purchase_confirm, ref: purchase_order_fifo2}
-
I check the "Approved" status of purchase order 1
-
!assert {model: purchase.order, id: purchase_order_fifo1}:
- state == 'approved'
-
Process the reception of purchase order 1
-
!python {model: stock.partial.picking}: |
pick_ids = self.pool.get('purchase.order').browse(cr, uid, ref("purchase_order_fifo1")).picking_ids
partial_id = self.create(cr, uid, {}, context={'active_model': 'stock.picking','active_ids': [pick_ids[0].id]})
self.do_partial(cr, uid, [partial_id])
-
Check the standard price of the product (fifo icecream)
-
!python {model: product.product}: |
assert self.browse(cr, uid, ref("product_fifo_icecream")).standard_price == 70.0, 'Standard price should not have changed!'
-
Process the reception of purchase order 2
-
!python {model: stock.partial.picking}: |
pick_ids = self.pool.get('purchase.order').browse(cr, uid, ref("purchase_order_fifo2")).picking_ids
partial_id = self.create(cr, uid, {}, context={'active_model': 'stock.picking','active_ids': [pick_ids[0].id]})
self.do_partial(cr, uid, [partial_id])
-
Check the standard price should not have changed
-
!python {model: product.product}: |
assert self.browse(cr, uid, ref("product_fifo_icecream")).standard_price == 70.0, 'Standard price as fifo price of second reception incorrect!'
-
Let us send some goods
-
!record {model: stock.picking, id: outgoing_fifo_shipment}:
type: out
-
Picking needs movement from stock
-
!record {model: stock.move, id: outgoing_shipment_fifo_icecream}:
picking_id: outgoing_fifo_shipment
product_id: product_fifo_icecream
product_uom: product.product_uom_kgm
product_qty: 20.0
type: out
-
I confirm outgoing shipment of 20 kg of FIFO Ice Cream.
-
!workflow {model: stock.picking, action: button_confirm, ref: outgoing_fifo_shipment}
-
Process the delivery of the outgoing shipment
-
!python {model: stock.partial.picking}: |
partial_id = self.create(cr, uid, {}, context={'active_model': 'stock.picking','active_ids': [ref("outgoing_fifo_shipment")], 'default_type':'out'})
self.do_partial(cr, uid, [partial_id])
-
Check 2 stock move matchings were created
-
!python {model: stock.picking}: |
assert len(self.browse(cr, uid, ref("outgoing_fifo_shipment")).move_lines[0].matching_ids_out) == 2, 'Should have created 2 matchings'

View File

@ -0,0 +1,110 @@
-
Set a product as using lifo price
-
!record {model: product.product, id: product_lifo_icecream}:
default_code: LIFO
name: LIFO Ice Cream
type: product
categ_id: product.product_category_1
list_price: 100.0
standard_price: 70.0
uom_id: product.product_uom_kgm
uom_po_id: product.product_uom_kgm
procure_method: make_to_stock
valuation: real_time
cost_method: lifo
property_stock_account_input: account.o_expense
property_stock_account_output: account.o_income
description: LIFO Ice Cream can be mass-produced and thus is widely available in developed parts of the world. Ice cream can be purchased in large cartons (vats and squrounds) from supermarkets and grocery stores, in smaller quantities from ice cream shops, convenience stores, and milk bars, and in individual servings from small carts or vans at public events.
-
I create a draft Purchase Order for first in move for 10 pieces at 60 euro
-
!record {model: purchase.order, id: purchase_order_lifo1}:
partner_id: base.res_partner_3
location_id: stock.stock_location_stock
pricelist_id: 1
order_line:
- product_id: product_lifo_icecream
product_qty: 10.0
product_uom: product.product_uom_categ_kgm
price_unit: 60.0
name: 'LIFO Ice Cream'
-
I create a draft Purchase Order for second shipment for 30 pieces at 80 euro
-
!record {model: purchase.order, id: purchase_order_lifo2}:
partner_id: base.res_partner_3
location_id: stock.stock_location_stock
pricelist_id: 1
order_line:
- product_id: product_lifo_icecream
product_qty: 30.0
product_uom: product.product_uom_categ_kgm
price_unit: 80.0
name: 'LIFO Ice Cream'
-
I confirm the first purchase order
-
!workflow {model: purchase.order, action: purchase_confirm, ref: purchase_order_lifo1}
-
I confirm the second purchase order
-
!workflow {model: purchase.order, action: purchase_confirm, ref: purchase_order_lifo2}
-
I check the "Approved" status of purchase order 1
-
!assert {model: purchase.order, id: purchase_order_lifo1}:
- state == 'approved'
-
Process the reception of purchase order 1
-
!python {model: stock.partial.picking}: |
pick_ids = self.pool.get('purchase.order').browse(cr, uid, ref("purchase_order_lifo1")).picking_ids
partial_id = self.create(cr, uid, {}, context={'active_model': 'stock.picking','active_ids': [pick_ids[0].id]})
self.do_partial(cr, uid, [partial_id])
-
Check the standard price of the product (lifo icecream)
-
!python {model: product.product}: |
assert self.browse(cr, uid, ref("product_lifo_icecream")).standard_price == 70.0, 'Standard price should not have changed!'
-
Process the reception of purchase order 2
-
!python {model: stock.partial.picking}: |
pick_ids = self.pool.get('purchase.order').browse(cr, uid, ref("purchase_order_lifo2")).picking_ids
partial_id = self.create(cr, uid, {}, context={'active_model': 'stock.picking','active_ids': [pick_ids[0].id]})
self.do_partial(cr, uid, [partial_id])
-
Check the standard price should not have changed
-
!python {model: product.product}: |
assert self.browse(cr, uid, ref("product_lifo_icecream")).standard_price == 70.0, 'Standard price as lifo price of second reception incorrect!'
-
Let us send some goods
-
!record {model: stock.picking, id: outgoing_lifo_shipment}:
type: out
-
Picking needs movement from stock
-
!record {model: stock.move, id: outgoing_shipment_lifo_icecream}:
picking_id: outgoing_lifo_shipment
product_id: product_lifo_icecream
product_uom: product.product_uom_kgm
product_qty: 20.0
type: out
-
I confirm outgoing shipment of 20 kg of LIFO Ice Cream.
-
!workflow {model: stock.picking, action: button_confirm, ref: outgoing_lifo_shipment}
-
Process the delivery of the outgoing shipment
-
!python {model: stock.partial.picking}: |
partial_id = self.create(cr, uid, {}, context={'active_model': 'stock.picking','active_ids': [ref("outgoing_lifo_shipment")], 'default_type':'out'})
self.do_partial(cr, uid, [partial_id])
-
Check only 1 stock move matching was created
-
!python {model: stock.picking}: |
assert len(self.browse(cr, uid, ref("outgoing_lifo_shipment")).move_lines[0].matching_ids_out) == 1, 'Should have created 1 matching'