From 7475d54cb5bfaeff98f5e8c1e8750e7769a8abc2 Mon Sep 17 00:00:00 2001 From: Josse Colpaert Date: Sat, 25 May 2013 15:16:39 +0200 Subject: [PATCH] [IMP] price_unit_out on stock move matchings as related field, adding test for cost method changing and returns, adding related field to stock move report bzr revid: jco@openerp.com-20130525131639-z3ekab24sp1jw91y --- addons/purchase/__openerp__.py | 2 + addons/purchase/test/average_price.yml | 4 +- addons/purchase/test/costmethodchange.yml | 142 ++++++++++++++++++ addons/purchase/test/fifo_price.yml | 47 +++--- addons/purchase/test/fifo_returns.yml | 97 ++++++++++++ addons/stock/report/report_stock_move.py | 3 +- .../stock/report/report_stock_move_view.xml | 23 ++- addons/stock/stock.py | 45 +++--- addons/stock/stock_fifo_lifo.py | 44 ++++-- 9 files changed, 349 insertions(+), 58 deletions(-) create mode 100644 addons/purchase/test/costmethodchange.yml create mode 100644 addons/purchase/test/fifo_returns.yml diff --git a/addons/purchase/__openerp__.py b/addons/purchase/__openerp__.py index 27fecb4c1bc..8d210b0f870 100644 --- a/addons/purchase/__openerp__.py +++ b/addons/purchase/__openerp__.py @@ -67,6 +67,8 @@ Dashboard / Reports for Purchase Management will include: 'res_config_view.xml', ], 'test': [ + 'test/fifo_returns.yml', + 'test/costmethodchange.yml', 'test/process/cancel_order.yml', 'test/process/rfq2order2done.yml', 'test/process/generate_invoice_from_reception.yml', diff --git a/addons/purchase/test/average_price.yml b/addons/purchase/test/average_price.yml index 678d2135b04..e14c1088494 100644 --- a/addons/purchase/test/average_price.yml +++ b/addons/purchase/test/average_price.yml @@ -75,10 +75,10 @@ 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 TODO the error message if the assert fails is wrong + Check the standard price - !python {model: product.product}: | - assert self.browse(cr, uid, ref("product_average_icecream")).standard_price == 75.0, 'After second reception, the standard price should be the weighted average of the 2 receptions' + assert self.browse(cr, uid, ref("product_average_icecream")).standard_price == 75.0, 'After second reception, we should have an average price of 75.0 on the product' - Create picking to send some goods - diff --git a/addons/purchase/test/costmethodchange.yml b/addons/purchase/test/costmethodchange.yml new file mode 100644 index 00000000000..8f10e7f6491 --- /dev/null +++ b/addons/purchase/test/costmethodchange.yml @@ -0,0 +1,142 @@ +- + Set a product as using average price. +- + !record {model: product.product, id: product_variable_icecream}: + default_code: AVG + name: Variable Ice Cream + type: product + categ_id: product.product_category_1 + 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 incoming shipment for 10 pieces at 60€ +- + !record {model: purchase.order, id: purchase_order_variable1}: + partner_id: base.res_partner_3 + location_id: stock.stock_location_stock + pricelist_id: 1 + order_line: + - product_id: product_variable_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 incoming shipment for 30 pieces at 80€ +- + !record {model: purchase.order, id: purchase_order_variable2}: + partner_id: base.res_partner_3 + location_id: stock.stock_location_stock + pricelist_id: 1 + order_line: + - product_id: product_variable_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_variable1} +- + I confirm the second purchase order +- + !workflow {model: purchase.order, action: purchase_confirm, ref: purchase_order_variable2} +- + I check the "Approved" status of purchase order 1 +- + !assert {model: purchase.order, id: purchase_order_variable1}: + - 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_variable1")).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 (variable icecream). +- + !python {model: product.product}: | + product = self.browse(cr, uid, ref("product_variable_icecream")) + assert product.qty_available == 10.0, 'Wrong quantity in stock after first reception' + assert product.standard_price == 60.0, 'Standard price should be the price of the first reception!' +- + 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_variable2")).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 and change the product's cost method towards fifo +- + !python {model: product.product}: | + assert self.browse(cr, uid, ref("product_variable_icecream")).standard_price == 75.0, 'After second reception, we should have an average price of 75.0 on the product' + self.write(cr, uid, [ref("product_variable_icecream")], {'cost_method': 'fifo'}, context=context) +- + Create picking to send some goods +- + !record {model: stock.picking, id: outgoing_variable_shipment}: + type: out +- + Create move for picking +- + !record {model: stock.move, id: outgoing_shipment_variable_icecream}: + picking_id: outgoing_variable_shipment + product_id: product_variable_icecream + product_uom: product.product_uom_kgm + product_qty: 20.0 + type: out +- + I confirm outgoing shipment of 20 kg of Variable Ice Cream. +- + !workflow {model: stock.picking, action: button_confirm, ref: outgoing_variable_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_variable_shipment")], 'default_type':'out'}) + self.do_partial(cr, uid, [partial_id]) +- + 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_variable_icecream")).standard_price == 75.0, 'Standard price should not have changed with outgoing picking!' + assert self.browse(cr, uid, ref("product_variable_icecream")).qty_available == 20.0, 'Pieces were not picked correctly as the quantity on hand is wrong' +- + I create a draft Purchase Order for third incoming shipment for 20 pieces at 60 euro +- + !record {model: purchase.order, id: purchase_order_variable3}: + partner_id: base.res_partner_3 + location_id: stock.stock_location_stock + pricelist_id: 1 + order_line: + - product_id: product_variable_icecream + product_qty: 20.0 + product_uom: product.product_uom_categ_kgm + price_unit: 60.0 + name: 'Variable Ice Cream' +- + I confirm this third purchase order +- + !workflow {model: purchase.order, action: purchase_confirm, ref: purchase_order_variable3} +- + Process the reception of purchase order 3 +- + !python {model: stock.partial.picking}: | + pick_ids = self.pool.get('purchase.order').browse(cr, uid, ref("purchase_order_variable3")).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]) +- + Change the cost method back to average and check price on product changed back to 70.0 = (20*60 + 10*60 + 30*80) / 60 +- + !python {model: product.product}: | + self.write(cr, uid, [ref("product_variable_icecream")], {'cost_method': 'average'}, context=context) + assert self.browse(cr, uid, ref("product_variable_icecream")).standard_price == 70.0, 'After third reception and changing to average again, we should have an average price of 70.0 on the product' \ No newline at end of file diff --git a/addons/purchase/test/fifo_price.yml b/addons/purchase/test/fifo_price.yml index 70f2e13ccc7..f2733b21616 100644 --- a/addons/purchase/test/fifo_price.yml +++ b/addons/purchase/test/fifo_price.yml @@ -17,7 +17,7 @@ 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 kg at 60 euro + I create a draft Purchase Order for first in move for 10 kg at 50 euro - !record {model: purchase.order, id: purchase_order_fifo1}: partner_id: base.res_partner_3 @@ -52,12 +52,15 @@ !assert {model: purchase.order, id: purchase_order_fifo1}: - state == 'approved' - - Process the reception of purchase order 1 + Process the reception of purchase order 1 and set date - !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]) + from datetime import * + #for move in pick_ids[0].move_lines: + # self.pool.get("stock.move").write(cr, uid, [move.id], {'date': (datetime.now()+timedelta(1)).strftime('%Y-%m-%d %H:%M:%S')}) - Check the standard price of the product (fifo icecream) - @@ -181,7 +184,7 @@ - Purchase order in usd - - I create a draft Purchase Order for 30000 g at 0.150 USD/g and 10 kg at 150 USD/kg + I create a draft Purchase Order for 30 kg at 0.150 USD/g and 10 kg at 150 USD/kg - !record {model: purchase.order, id: purchase_order_fifo_usd}: partner_id: base.res_partner_3 @@ -213,7 +216,7 @@ print partial_id self.do_partial(cr, uid, [partial_id]) - - We create delivery order of 49500 gram + We create delivery order of 49.500 kg - !record {model: stock.picking, id: outgoing_fifo_shipment_cur}: type: out @@ -223,8 +226,8 @@ !record {model: stock.move, id: outgoing_shipment_fifo_icecream_cur}: picking_id: outgoing_fifo_shipment_cur product_id: product_fifo_icecream - product_uom: product.product_uom_gram - product_qty: 49500 + product_uom: product.product_uom_kgm + product_qty: 49.500 type: out - Process the delivery of the outgoing shipment @@ -248,7 +251,7 @@ for move in self.pool.get("stock.move").browse(cr, uid, list): print move.price_unit, move.product_qty, move.qty_remaining, move.type, move.date - - Do a delivery of an extra 10 kg INSTEAD OF RETURN + Do a delivery of an extra 10 kg - !record {model: stock.picking, id: outgoing_fifo_shipment_ret}: type: out @@ -275,7 +278,6 @@ Check rounded price is 150.0 / 1.2834 - !python {model: product.product}: | - print round(self.browse(cr, uid, ref("product_fifo_icecream")).standard_price, 2) assert round(self.browse(cr, uid, ref("product_fifo_icecream")).standard_price) == round(150.0 / 1.2834) - Print price @@ -288,7 +290,7 @@ for move in self.pool.get("stock.move").browse(cr, uid, list): print move.price_unit, move.product_qty, move.qty_remaining, move.type, move.date - - Let us create some outs to get negative stock. Create outpicking. We create delivery order of 100 kg + Let us create some outs to get negative stock. Create outpicking. We create delivery order of 200 kg, but will pick only 100 kg - !record {model: stock.picking, id: outgoing_fifo_shipment_neg}: type: out @@ -299,10 +301,10 @@ picking_id: outgoing_fifo_shipment_neg product_id: product_fifo_icecream product_uom: product.product_uom_kgm - product_qty: 100.0 + product_qty: 200.0 type: out - - Let us create another out of 50 kg + Let us create another out of 400 kg, but will pick only 50 kg - !record {model: stock.picking, id: outgoing_fifo_shipment_neg2}: type: out @@ -313,15 +315,19 @@ picking_id: outgoing_fifo_shipment_neg2 product_id: product_fifo_icecream product_uom: product.product_uom_kgm - product_qty: 50.0 + product_qty: 400.0 type: out - Process the delivery of the outgoing shipments - !python {model: stock.partial.picking}: | partial_id = self.create(cr, uid, {}, context={'active_model': 'stock.picking','active_ids': [ref("outgoing_fifo_shipment_neg")], 'default_type':'out'}) + line = self.browse(cr, uid, partial_id, context=context).move_ids[0] + self.pool.get("stock.partial.picking.line").write(cr, uid, [line.id], {'quantity':100}) self.do_partial(cr, uid, [partial_id]) partial_id = self.create(cr, uid, {}, context={'active_model': 'stock.picking','active_ids': [ref("outgoing_fifo_shipment_neg2")], 'default_type':'out'}) + line = self.browse(cr, uid, partial_id, context=context).move_ids[0] + self.pool.get("stock.partial.picking.line").write(cr, uid, [line.id], {'quantity':50}) self.do_partial(cr, uid, [partial_id]) - Print price @@ -334,7 +340,7 @@ for move in self.pool.get("stock.move").browse(cr, uid, list): print move.price_unit, move.product_qty, move.qty_remaining, move.type, move.date - - Receive purchase order with 100 kgm FIFO Ice Cream at 50 euro/kg + Receive purchase order with 50 kg FIFO Ice Cream at 50 euro/kg - !record {model: purchase.order, id: purchase_order_fifo_neg}: partner_id: base.res_partner_3 @@ -342,7 +348,7 @@ pricelist_id: 1 order_line: - product_id: product_fifo_icecream - product_qty: 100.0 + product_qty: 50.0 product_uom: product.product_uom_categ_kgm price_unit: 50.0 name: 'FIFO Ice Cream' @@ -358,13 +364,12 @@ partial_id = self.create(cr, uid, {}, context={'active_model': 'stock.picking','active_ids': [pick_ids[0].id]}) self.do_partial(cr, uid, [partial_id]) - - Assert price on product is 50 + Assert price on product is still the old price as the out move has not been received fully yet - !python {model: product.product}: | - assert self.browse(cr, uid, ref("product_fifo_icecream")).standard_price == 50.0 - + assert round(self.browse(cr, uid, ref("product_fifo_icecream")).standard_price) == round(150.0 / 1.2834) - - Receive purchase order with 100 kgm FIFO Ice Cream at 80 euro/kg + Receive purchase order with 60 kg FIFO Ice Cream at 80 euro/kg - !record {model: purchase.order, id: purchase_order_fifo_neg2}: partner_id: base.res_partner_3 @@ -372,7 +377,7 @@ pricelist_id: 1 order_line: - product_id: product_fifo_icecream - product_qty: 100.0 + product_qty: 60.0 product_uom: product.product_uom_categ_kgm price_unit: 80 name: 'FIFO Ice Cream' @@ -388,12 +393,12 @@ partial_id = self.create(cr, uid, {}, context={'active_model': 'stock.picking','active_ids': [pick_ids[0].id]}) self.do_partial(cr, uid, [partial_id]) - - Print price + The price of the product should have changed back to 65.0 - !python {model: product.product}: | print self.browse(cr, uid, ref("product_fifo_icecream")).standard_price print self.browse(cr, uid, ref("product_fifo_icecream")).qty_available print self.browse(cr, uid, ref("product_fifo_icecream"), context={'force_company': 1}).qty_available - list = self.pool.get("stock.move").search(cr, uid, [('product_id','=', ref("product_fifo_icecream"))]) + list = self.pool.get("stock.move").search(cr, uid, [('product_id','=', ref("product_fifo_icecream"))], order='date') for move in self.pool.get("stock.move").browse(cr, uid, list): print move.price_unit, move.product_qty, move.qty_remaining, move.type, move.date \ No newline at end of file diff --git a/addons/purchase/test/fifo_returns.yml b/addons/purchase/test/fifo_returns.yml new file mode 100644 index 00000000000..e3c7c78badc --- /dev/null +++ b/addons/purchase/test/fifo_returns.yml @@ -0,0 +1,97 @@ +- + Set a product as using fifo price +- + !record {model: product.product, id: product_fiforet_icecream}: + default_code: FIFO + name: FIFO Ice Cream + type: product + categ_id: product.product_category_1 + uom_id: product.product_uom_kgm + uom_po_id: product.product_uom_kgm + procure_method: make_to_stock + cost_method: fifo + valuation: real_time + 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 kg at 50 euro +- + !record {model: purchase.order, id: purchase_order_fiforet1}: + partner_id: base.res_partner_3 + location_id: stock.stock_location_stock + pricelist_id: 1 + order_line: + - product_id: product_fiforet_icecream + product_qty: 10.0 + product_uom: product.product_uom_categ_kgm + price_unit: 50.0 + name: 'FIFO Ice Cream' +- + I create a draft Purchase Order for second shipment for 30 kg at 80 euro +- + !record {model: purchase.order, id: purchase_order_fiforet2}: + partner_id: base.res_partner_3 + location_id: stock.stock_location_stock + pricelist_id: 1 + order_line: + - product_id: product_fiforet_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_fiforet1} +- + 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_fiforet1")).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_fiforet_icecream")).standard_price == 0.0, 'Standard price should not have changed!' +- + I confirm the second purchase order +- + !workflow {model: purchase.order, action: purchase_confirm, ref: purchase_order_fiforet2} +- + 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_fiforet2")).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]) +- + Return the goods of purchase order 2 +- + !python {model: stock.return.picking}: | + pick_ids = self.pool.get('purchase.order').browse(cr, uid, ref("purchase_order_fiforet2")).picking_ids + return_id = self.create(cr, uid, {}, context={'active_model':'stock.picking', 'active_id': pick_ids[0].id}) + res = self.create_returns(cr, uid, [return_id], context={'active_id': pick_ids[0].id}) + return_move_id = self.pool.get("stock.move").search(cr, uid, [('move_returned_from','=', pick_ids[0].move_lines[0].id)]) + + movepick_obj = self.pool.get("stock.partial.move") + movepick_id = movepick_obj.create(cr, uid, {}, context={'active_model': 'stock.move', 'active_ids': return_move_id}) + movepick_obj.do_partial(cr, uid, [movepick_id]) +- + Check the standard price of the product (fifo return icecream) changed to 70.0 +- + !python {model: product.product}: | + print self.browse(cr, uid, ref("product_fiforet_icecream")).standard_price + assert self.browse(cr, uid, ref("product_fiforet_icecream")).standard_price == 70.0, 'Standard price should have changed to 70.0!' +- + Print price +- + !python {model: product.product}: | + print self.browse(cr, uid, ref("product_fiforet_icecream")).standard_price + print self.browse(cr, uid, ref("product_fiforet_icecream")).qty_available + print self.browse(cr, uid, ref("product_fiforet_icecream"), context={'force_company': 1}).qty_available + list = self.pool.get("stock.move").search(cr, uid, [('product_id','=', ref("product_fiforet_icecream"))]) + for move in self.pool.get("stock.move").browse(cr, uid, list): + print move.price_unit, move.product_qty, move.qty_remaining, move.type, move.date \ No newline at end of file diff --git a/addons/stock/report/report_stock_move.py b/addons/stock/report/report_stock_move.py index c05cdc1c93d..4a5315ea887 100644 --- a/addons/stock/report/report_stock_move.py +++ b/addons/stock/report/report_stock_move.py @@ -261,8 +261,7 @@ class report_stock_valuation(osv.osv): 'location_dest_type': fields.selection([('supplier', 'Supplier Location'), ('view', 'View'), ('internal', 'Internal Location'), ('customer', 'Customer Location'), ('inventory', 'Inventory'), ('procurement', 'Procurement'), ('production', 'Production'), ('transit', 'Transit Location for Inter-Companies Transfers')], 'Location Destination Type', required=True), 'location_src_type': fields.selection([('supplier', 'Supplier Location'), ('view', 'View'), ('internal', 'Internal Location'), ('customer', 'Customer Location'), ('inventory', 'Inventory'), ('procurement', 'Procurement'), ('production', 'Production'), ('transit', 'Transit Location for Inter-Companies Transfers')], 'Location Source Type', required=True), 'match': fields.many2one('stock.move', 'Match', readonly=True), - 'related': fields.related('match', 'picking_id', 'name', type='text', readonly=True), - #'funct': fields.function(_get_properties, type='float') + 'related_move_in': fields.related('match', 'picking_id', 'name', type='text', readonly=True), } diff --git a/addons/stock/report/report_stock_move_view.xml b/addons/stock/report/report_stock_move_view.xml index 545cfbcc170..457418c9842 100644 --- a/addons/stock/report/report_stock_move_view.xml +++ b/addons/stock/report/report_stock_move_view.xml @@ -205,6 +205,7 @@ + @@ -247,6 +248,7 @@ + @@ -280,9 +282,28 @@ the stock is calculated based on the prices of the stock moves depending on the costing method of the product (FIFO/LIFO/Average/standard) and not just the standard price of the product. Sort and group by selection criteria in order to better analyse and manage your company activities. + + + + Stock Inventory Valuation Analysis + report.stock.valuation + form + tree,graph + + {'contact_display': 'partner', 'search_default_real':1, +'search_default_year':1,'search_default_month':1, 'search_default_location_type_internal':1,'search_default_group_product':1,'group_by':[], 'group_by_no_leaf':0} + Stock Valuation Analysis allows you to easily check and analyse your company stock levels. The difference with the Inventory Analysis, is that + the stock is calculated based on the prices of the stock moves depending on the costing method of the product (FIFO/LIFO/Average/standard) and not just the standard price of the product. + Sort and group by selection criteria in order to better analyse and manage your company activities. + + + + - + \ No newline at end of file diff --git a/addons/stock/stock.py b/addons/stock/stock.py index fc1be4f1e87..1e136938a72 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -2676,7 +2676,7 @@ class stock_move(osv.osv): and creates the necessary stock move matchings It returns a list of tuples with (move_id, match_id) - + which can be used for generating the accounting entries ''' product_obj = self.pool.get('product.product') currency_obj = self.pool.get('res.currency') @@ -2698,6 +2698,9 @@ class stock_move(osv.osv): product = product_obj.browse(cr, uid, move.product_id.id, context=ctx) cost_method = product.cost_method product_uom_qty = uom_obj._compute_qty(cr, uid, product_uom, product_qty, product.uom_id.id) + if not product.id in product_avail: + product_avail[product.id] = product.qty_available + # Check if out -> do stock move matchings and if fifo/lifo -> update price if move.location_id.usage == 'internal' and move.location_dest_id.usage != 'internal': @@ -2705,13 +2708,12 @@ class stock_move(osv.osv): fifo = (cost_method != 'lifo') tuples = product_obj.get_stock_matchings_fifolifo(cr, uid, [product.id], product_qty, fifo, product_uom, move.company_id.currency_id.id, context=ctx) #TODO Would be better to use price_currency_id for migration? - print tuples price_amount = 0.0 amount = 0.0 #Write stock matchings for match in tuples: matchvals = {'move_in_id': match[0], 'qty': match[1], - 'move_out_id': move.id, 'price_unit_out': match[2]} + 'move_out_id': move.id} match_id = matching_obj.create(cr, uid, matchvals, context=context) res[move.id].append(match_id) move_in = self.browse(cr, uid, match[0], context=context) @@ -2725,10 +2727,10 @@ class stock_move(osv.osv): new_price = uom_obj._compute_price(cr, uid, product.uom_id.id, product.standard_price, product_uom) self.write(cr, uid, move.id, {'price_unit': new_price}, context=ctx) - - - if not product.id in product_avail: - product_avail[product.id] = product.qty_available + #Adjust product_avail when not average and move returned from + if (not move.move_returned_from or product.cost_method != 'average'): + product_avail[product.id] -= product_uom_qty + #Check if in => if price 0.0, take standard price / Update price when average price and price on move != standard price if move.location_id.usage != 'internal' and move.location_dest_id.usage == 'internal': if move.price_unit == 0.0: @@ -2743,25 +2745,26 @@ class stock_move(osv.osv): else: new_std_price = move.price_unit product_obj.write(cr, uid, [product.id], {'standard_price': new_std_price}, context=ctx) - # Should create the stock move matchings for outs for the negative stock + # Should create the stock move matchings for previous outs for the negative stock that can be matched with is in if product_avail[product.id] < 0.0: #Search for most recent out moves until at least one matching has been found => order date desc moves = self.search(cr, uid, [('company_id', '=', move.company_id.id), ('state','=', 'done'), ('location_id.usage','=','internal'), ('location_dest_id.usage', '!=', 'internal'), ('product_id', '=', move.product_id.id), ('qty_remaining', '>', 0.0)], order='date', context=ctx) qty_to_go = move.product_qty for out_mov in self.browse(cr, uid, moves, context=ctx): + if qty_to_go <= 0.0: + break out_qty_converted = uom_obj._compute_qty(cr, uid, out_mov.product_uom.id, out_mov.qty_remaining, move.product_uom.id) qty = 0.0 if out_qty_converted <= qty_to_go: qty = out_qty_converted elif qty_to_go > 0.0: qty = qty_to_go - new_price = uom_obj._compute_price(cr, uid, move.product_uom.id, move.price_unit, - out_mov.product_uom.id) - matchvals = {'move_in_id': move.id, 'qty': qty, - 'move_out_id': out_mov.id, 'price_unit_out': new_price} + #revert_qty = uom_obj._compute_qty(cr, uid, move.product_uom.id, qty, out_mov.product_uom.id) + revert_qty = (qty / out_qty_converted) * out_mov.qty_remaining + matchvals = {'move_in_id': move.id, 'qty': revert_qty, + 'move_out_id': out_mov.id} match_id = matching_obj.create(cr, uid, matchvals, context=context) - res[move.id].append(match_id) qty_to_go -= qty #Need to re-calculate total price of every out_move if FIFO/LIFO @@ -2770,15 +2773,17 @@ class stock_move(osv.osv): amount = 0.0 total_price = 0.0 for match in matching_obj.browse(cr, uid, matches, context=context): - in_mov = self.browse(cr, uid, match.move_in_id.id, context=context) - amount += uom_obj._compute_qty(cr, uid, in_mov.product_uom.id, in_mov.product_qty, out_mov.product_uom.id) - total_price += amount * uom_obj._compute_price(cr, uid, in_mov.product_uom.id, in_mov.price_unit, out_mov.product_uom.id) + amount += match.qty + total_price += match.qty * match.price_unit_out if amount > 0.0: self.write(cr, uid, [out_mov.id], {'price_unit': total_price / amount}, context=context) - product_obj.write(cr, uid, [product.id], {'standard_price': total_price / amount}, context=ctx) - - product_avail[product.id] += product.qty_available - + if amount >= out_mov.product_qty: + print "Update product when negative" + print total_price + print amount + print out_mov.product_qty + product_obj.write(cr, uid, [product.id], {'standard_price': total_price / amount}, context=ctx) + product_avail[product.id] += product_uom_qty #The return of average products at average price could be made optional if move.location_id.usage == 'internal' and move.location_dest_id.usage != 'internal' and cost_method == 'average' and move.move_returned_from: move_orig = move.move_returned_from diff --git a/addons/stock/stock_fifo_lifo.py b/addons/stock/stock_fifo_lifo.py index 421c81df337..ac517c87b54 100644 --- a/addons/stock/stock_fifo_lifo.py +++ b/addons/stock/stock_fifo_lifo.py @@ -38,27 +38,25 @@ class product_product (osv.osv): #TODO Need force_company in context? if 'force_company' not in context: company_id = self.pool.get("res.users").browse(cr, uid, uid, context=context).company_id.id - else: company_id = context['force_company'] for prod in self.browse(cr, uid, ids, context=context): if prod.cost_method == 'average': - mov_ids = move_obj.search(cr, uid, [('company_id', '=', company_id), ('state', '=', 'done'), ('qty_remaining', '>', 0.0)], context=context) + mov_ids = move_obj.search(cr, uid, [('product_id', '=', prod.id), ('company_id', '=', company_id), ('state', '=', 'done'), ('qty_remaining', '>', 0.0)], context=context) #TODO Need to convert to all prices to UoM of stock move for move in move_obj.browse(cr, uid, mov_ids, context=context): - new_price = uom_obj._compute_price(cr, uid, move.product_uom.id, move.price_unit, prod.uom_id.id) - move.write({'price_unit': new_price}) + new_price = uom_obj._compute_price(cr, uid, move.product_uom.id, prod.standard_price, prod.uom_id.id) + move_obj.write(cr, uid, [move.id], {'price_unit': new_price}, context=context) #We need to recalculate the average on all products from all stock moves in if vals["cost_method"] == 'average': #Search all done in moves - mov_ids = move_obj.search(cr, uid, [('company_id', '=', company_id), ('state', '=', 'done'), ('location_dest_id.usage', '=', 'internal'), + mov_ids = move_obj.search(cr, uid, [('product_id', '=', prod.id), ('company_id', '=', company_id), ('state', '=', 'done'), ('location_dest_id.usage', '=', 'internal'), ('location_id.usage', '!=', 'internal')], context=context) qty = 0.0 total_price = 0.0 for move in move_obj.browse(cr, uid, mov_ids, context=context): total_price += move.product_qty * move.price_unit qty += uom_obj._compute_qty(cr, uid, move.product_uom.id, move.product_qty, prod.uom_id.id) - print "Qty and Total Price:", qty, total_price if qty > 0.0: prod_obj.write(cr, uid, [prod.id], {'standard_price': total_price / qty}, context=context) res = super(product_product, self).write(cr, uid, ids, vals, context=context) @@ -158,16 +156,13 @@ class stock_move(osv.osv): if move_in: #Search all matchings matches = match_obj.search(cr, uid, [('move_in_id', '=', move.id)], context=context) - print [(x.move_in_id.product_qty, x.qty, x.move_in_id.price_unit, x.move_in_id.date) for x in match_obj.browse(cr, uid, matches, context=context)] - qty = move.product_qty for match in match_obj.browse(cr, uid, matches, context=context): qty -= uom_obj._compute_qty(cr, uid, match.move_out_id.product_uom.id, match.qty, move.product_uom.id) res[move.id] = qty elif move_out: - #Search all matchings, but from the other side + #Search all matchings, but from the out side matches = match_obj.search(cr, uid, [('move_out_id', '=', move.id)], context=context) - print [(x.move_in_id.product_qty, x.qty, x.move_in_id.price_unit, x.move_in_id.date) for x in match_obj.browse(cr, uid, matches, context=context)] qty = move.product_qty for match in match_obj.browse(cr, uid, matches, context=context): qty -= match.qty @@ -182,15 +177,40 @@ class stock_move(osv.osv): } - class stock_move_matching(osv.osv): _name = "stock.move.matching" _description = "Stock move matchings" + + + def _get_unit_price_out (self, cr, uid, ids, field_names, arg, context=None): + res = {} + uom_obj = self.pool.get("product.uom") + for match in self.browse(cr, uid, ids, context=context): + res[match.id] = uom_obj._compute_price(cr, uid, match.move_in_id.product_uom.id, match.price_unit, match.move_out_id.product_uom.id) + return res + + def _get_matches(self, cr, uid, ids, context=None): + res = [] + move_obj = self.pool.get("stock.move") + for move in move_obj.browse(cr, uid, ids, context=context): + move_in = move.location_id and move.location_dest_id and move.location_id.usage != 'internal' and move.location_dest_id.usage == 'internal' + if move_in: + matches = self.pool.get("stock.move.matching").search(cr, uid, [('move_in_id', '=', move.id)], context=context) + res += matches + return res + + def _get_selfmatches(self, cr, uid, ids, context=None): + return ids + + _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.float('Quantity in UoM of out', required=True), 'price_unit':fields.related('move_in_id', 'price_unit', string="Unit price", type="float"), - 'price_unit_out': fields.float('Unit price out'), + 'price_unit_out': fields.function(_get_unit_price_out, type="float", string="Price in UoM of out move", + store = {'stock.move': (_get_matches, ['price_unit', 'product_uom'], 10), + 'stock.move.matching': (_get_selfmatches, ['move_in_id', 'qty', 'move_out_id'], 10)},), } +