diff --git a/addons/purchase/test/fifo_price.yml b/addons/purchase/test/fifo_price.yml index 42e8897eb01..3bf162ae50a 100644 --- a/addons/purchase/test/fifo_price.yml +++ b/addons/purchase/test/fifo_price.yml @@ -255,9 +255,11 @@ Check rounded price is 150.0 / 1.2834 - !python {model: product.product}: | - assert round(self.browse(cr, uid, ref("product_fifo_icecream")).standard_price) == round(150.0 / 1.2834), "Product price not updated accordingly. %s found instead of %s" %(self.browse(cr, uid, ref("product_fifo_icecream")).standard_price, round(150.0/1.2834)) + product = self.browse(cr, uid, ref("product_fifo_icecream")) + assert round(product.standard_price) == round(150.0 / 1.2834), "Product price not updated accordingly. %s found instead of %s" %(product.standard_price, round(150.0/1.2834)) + assert product.qty_available == 0.0, 'Wrong quantity in stock after first reception' - - Let us create some outs to get negative stock. Create outpicking. We create delivery order of 200 kg, but will pick only 100 kg + Let us create some outs to get negative stock. Create outpicking. We create delivery order of 100 kg. - !record {model: stock.picking, id: outgoing_fifo_shipment_neg}: picking_type_id: stock.picking_type_out @@ -268,12 +270,18 @@ picking_id: outgoing_fifo_shipment_neg product_id: product_fifo_icecream product_uom: product.product_uom_kgm - product_uom_qty: 200.0 + product_uom_qty: 100.0 location_id: stock.stock_location_stock location_dest_id: stock.stock_location_customers picking_type_id: stock.picking_type_out - - Let us create another out of 400 kg, but will pick only 50 kg + Process the delivery of the first outgoing shipment +- + !python {model: stock.picking}: | + picking_obj = self.browse(cr, uid, ref("outgoing_fifo_shipment_neg")) + picking_obj.do_partial(context=context) +- + Let us create another out of 400 kg - !record {model: stock.picking, id: outgoing_fifo_shipment_neg2}: picking_type_id: stock.picking_type_out @@ -292,8 +300,6 @@ Process the delivery of the outgoing shipments - !python {model: stock.picking}: | - picking_obj = self.browse(cr, uid, ref("outgoing_fifo_shipment_neg")) - picking_obj.do_partial(context=context) picking_obj1 = self.browse(cr, uid, ref("outgoing_fifo_shipment_neg2")) picking_obj1.do_partial(context=context) - diff --git a/addons/stock/stock.py b/addons/stock/stock.py index 5d3c79417e5..82b91d8f66c 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -167,6 +167,7 @@ class stock_quant(osv.osv): # Used for negative quants to reconcile after compensated by a new positive one 'propagated_from_id': fields.many2one('stock.quant', 'Linked Quant', help='The negative quant this is coming from'), + 'negative_dest_location_id': fields.many2one('stock.location', 'Destination Location', help='Technical field used to record the destination location of a move that created a negative quant'), } _defaults = { @@ -260,6 +261,7 @@ class stock_quant(osv.osv): negative_vals['location_id'] = move.location_id.id negative_vals['qty'] = -qty negative_vals['cost'] = price_unit + negative_vals['negative_dest_location_id'] = move.location_dest_id.id negative_quant_id = self.create(cr, uid, negative_vals, context=context) vals.update({'propagated_from_id': negative_quant_id}) @@ -283,6 +285,20 @@ class stock_quant(osv.osv): move = m return move + 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) + self._quant_split(cr, uid, quant, qty, context=context) + remaining_to_solve_quant = self._quant_split(cr, uid, to_solve_quant, qty, context=context) + remaining_neg_quant = self._quant_split(cr, uid, quant_neg, -qty, context=context) + #if the reconciliation was not complete, we need to link together the remaining parts + if remaining_to_solve_quant and remaining_neg_quant: + self.write(cr, uid, remaining_to_solve_quant.id, {'propagated_from_id': remaining_neg_quant.id}, context=context) + #delete the reconciled quants, as it is replaced by the solving quant + self.unlink(cr, SUPERUSER_ID, [quant_neg.id, to_solve_quant.id], context=context) + #call move_single_quant to ensure recursivity if necessary and do the stock valuation + self.move_single_quant(cr, uid, quant, qty, move, context=context) + return remaining_to_solve_quant + def _quant_reconcile_negative(self, cr, uid, quant, context=None): """ When new quant arrive in a location, try to reconcile it with @@ -292,27 +308,14 @@ class stock_quant(osv.osv): if quant.location_id.usage != 'internal': return False quants = self.quants_get(cr, uid, quant.location_id, quant.product_id, quant.qty, [('qty', '<', '0')], context=context) - result = False for quant_neg, qty in quants: if not quant_neg: continue - result = True to_solve_quant = self.search(cr, uid, [('propagated_from_id', '=', quant_neg.id), ('id', '!=', quant.id)], context=context) if not to_solve_quant: continue to_solve_quant = self.browse(cr, uid, to_solve_quant[0], context=context) - move = self._get_latest_move(cr, uid, to_solve_quant, context=context) - self._quant_split(cr, uid, quant, qty, context=context) - remaining_to_solve_quant = self._quant_split(cr, uid, to_solve_quant, qty, context=context) - remaining_neg_quant = self._quant_split(cr, uid, quant_neg, -qty, context=context) - #if the reconciliation was not complete, we need to link together the remaining parts - if remaining_to_solve_quant and remaining_neg_quant: - self.write(cr, uid, remaining_to_solve_quant.id, {'propagated_from_id': remaining_neg_quant.id}, context=context) - #delete the reconciled quants, as it is replaced by the solving quant - self.unlink(cr, SUPERUSER_ID, [quant_neg.id, to_solve_quant.id], context=context) - #call move_single_quant to ensure recursivity if necessary and do the stock valuation - self.move_single_quant(cr, uid, quant, qty, move, context=context) - return result + self._reconcile_single_negative_quant(cr, uid, to_solve_quant, quant, quant_neg, qty, context=context) def _price_update(self, cr, uid, quant, newprice, context=None): self.write(cr, uid, [quant.id], {'cost': newprice}, context=context) @@ -1508,7 +1511,7 @@ class stock_move(osv.osv): # quants = quant_obj.quants_get(cr, uid, move.location_id, move.product_id, qty, context=context) # quant_obj.quants_move(cr, uid, quants, move, location_dest_id, context=context) # should replace the above 2 lines - domain = ['|', ('reservation_id', '=', False), ('reservation_id', '=', move.id)] + domain = ['|', ('reservation_id', '=', False), ('reservation_id', '=', move.id), ('qty', '>', 0)] prefered_order = 'reservation_id' # if lot_id: # prefered_order = 'lot_id<>' + lot_id + ", " + prefered_order diff --git a/addons/stock_account/stock_account.py b/addons/stock_account/stock_account.py index b4d94433f31..adcf6095ba1 100644 --- a/addons/stock_account/stock_account.py +++ b/addons/stock_account/stock_account.py @@ -19,23 +19,13 @@ # ############################################################################## -from datetime import datetime -from dateutil.relativedelta import relativedelta import time -from operator import itemgetter -from itertools import groupby from openerp.osv import fields, osv from openerp.tools.translate import _ -from openerp import netsvc -from openerp import tools -from openerp.tools import float_compare, DEFAULT_SERVER_DATETIME_FORMAT -import openerp.addons.decimal_precision as dp import logging _logger = logging.getLogger(__name__) - - #---------------------------------------------------------- # Stock Location #---------------------------------------------------------- @@ -44,12 +34,12 @@ class stock_location(osv.osv): _inherit = "stock.location" _columns = { - 'valuation_in_account_id': fields.many2one('account.account', 'Stock Valuation Account (Incoming)', domain = [('type','=','other')], + 'valuation_in_account_id': fields.many2one('account.account', 'Stock Valuation Account (Incoming)', domain=[('type', '=', 'other')], 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 = [('type','=','other')], + 'valuation_out_account_id': fields.many2one('account.account', 'Stock Valuation Account (Outgoing)', domain=[('type', '=', 'other')], 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. " @@ -63,7 +53,6 @@ class stock_location(osv.osv): class stock_quant(osv.osv): _inherit = "stock.quant" - 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'): @@ -170,7 +159,7 @@ class stock_quant(osv.osv): 'name': move.name, 'product_id': quant.product_id.id, 'quantity': quant.qty, - 'product_uom_id': quant.product_id.uom_id.id, + 'product_uom_id': quant.product_id.uom_id.id, 'ref': move.picking_id and move.picking_id.name or False, 'date': time.strftime('%Y-%m-%d'), 'partner_id': partner_id, @@ -181,7 +170,7 @@ class stock_quant(osv.osv): 'name': move.name, 'product_id': quant.product_id.id, 'quantity': quant.qty, - 'product_uom_id': quant.product_id.uom_id.id, + 'product_uom_id': quant.product_id.uom_id.id, 'ref': move.picking_id and move.picking_id.name or False, 'date': time.strftime('%Y-%m-%d'), 'partner_id': partner_id, @@ -197,6 +186,18 @@ class stock_quant(osv.osv): 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) + + 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 + remaining_to_solve_quant = super(stock_quant, self)._reconcile_single_negative_quant(cr, uid, to_solve_quant, quant, quant_neg, qty, context=context) + #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) + class stock_move(osv.osv): _inherit = "stock.move" @@ -204,6 +205,20 @@ class stock_move(osv.osv): super(stock_move, self).action_done(cr, uid, ids, context=context) self.product_price_update(cr, uid, ids, context=context) + def _store_average_cost_price(self, cr, uid, move, context=None): + ''' move is a browe record ''' + product_obj = self.pool.get('product.product') + 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) + def product_price_update(self, cr, uid, ids, context=None): ''' This method adapts the price on the product when necessary @@ -228,14 +243,5 @@ class stock_move(osv.osv): #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. if move.product_id.cost_method == 'real' and move.location_dest_id.usage != 'internal': - if any([q.qty <= 0 for q in move.quant_ids]): - #if there is a negative quant, the standard price shouldn't be updated - continue - #get the average price of the move - #Note: here we can't use the 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 cost 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) + #store the average price of the move on the move and product form + self._store_average_cost_price(cr, uid, move, context=context)