From f90c341a24945feb6936f954f8e8e9c78e0cfe3b Mon Sep 17 00:00:00 2001 From: Josse Colpaert Date: Tue, 24 Sep 2013 16:32:20 +0200 Subject: [PATCH] [WIP] Negative quants and packing operations bzr revid: jco@openerp.com-20130924143220-dcm9u876tfbxzayz --- addons/purchase/test/fifo_price.yml | 20 +- addons/stock/__openerp__.py | 1 + addons/stock/stock.py | 190 ++++++++++++------ addons/stock/test/packing.yml | 6 +- addons/stock/test/packingneg.yml | 113 +++++++++++ addons/stock_account/stock_account.py | 10 +- addons/stock_complex_routes/test/dropship.yml | 7 - 7 files changed, 249 insertions(+), 98 deletions(-) create mode 100644 addons/stock/test/packingneg.yml diff --git a/addons/purchase/test/fifo_price.yml b/addons/purchase/test/fifo_price.yml index c34f55fb03c..50704435fd6 100644 --- a/addons/purchase/test/fifo_price.yml +++ b/addons/purchase/test/fifo_price.yml @@ -323,13 +323,9 @@ Process the delivery of the outgoing shipments - !python {model: stock.picking}: | - quant_obj = self.pool.get("stock.quant") - quants = quant_obj.search(cr, uid, [('product_id','=',ref("product_fifo_negative"))]) picking_obj1 = self.browse(cr, uid, ref("outgoing_fifo_shipment_neg2")) - print "Quants:", [(x.qty, x.location_id.name, x.in_date, x.cost, x.id) for x in quant_obj.browse(cr, uid, quants)] picking_obj1.do_partial(context=context) - quants = quant_obj.search(cr, uid, [('product_id','=',ref("product_fifo_negative"))]) - print "Quants:", [(x.qty, x.location_id.name, x.in_date, x.cost, x.id) for x in quant_obj.browse(cr, uid, quants)] + - Receive purchase order with 50 kg FIFO Ice Cream at 50 euro/kg - @@ -351,19 +347,13 @@ Process the reception of purchase order 1 - !python {model: stock.picking}: | - quant_obj = self.pool.get("stock.quant") - quants = quant_obj.search(cr, uid, [('product_id','=',ref("product_fifo_negative"))]) picking_obj = self.pool.get('purchase.order').browse(cr, uid, ref("purchase_order_fifo_neg")).picking_ids[0] - quants = quant_obj.search(cr, uid, [('product_id','=',ref("product_fifo_negative"))]) - print "Quants:", [(x.qty, x.location_id.name, x.in_date, x.cost, x.id) for x in quant_obj.browse(cr, uid, quants)] picking_obj.do_partial(context=context) - Assert price on product is still the old price as the out move has not been received fully yet - !python {model: product.product}: | - quant_obj = self.pool.get("stock.quant") - quants = quant_obj.search(cr, uid, [('product_id','=',ref("product_fifo_negative"))]) - print "Quants:", [(x.qty, x.location_id.name, x.in_date, x.cost, x.id) for x in quant_obj.browse(cr, uid, quants)] + assert self.browse(cr, uid, ref("product_fifo_negative")).standard_price == 70, 'The product price should not have been updated' - Receive purchase order with 60 kg FIFO Ice Cream at 80 euro/kg @@ -386,16 +376,10 @@ Process the reception of purchase order 2 - !python {model: stock.picking}: | - quant_obj = self.pool.get("stock.quant") - quants = quant_obj.search(cr, uid, [('product_id','=',ref("product_fifo_negative"))]) - print "Quants:", [(x.qty, x.location_id.name, x.in_date, x.cost, x.id) for x in quant_obj.browse(cr, uid, quants)] picking_obj = self.pool.get('purchase.order').browse(cr, uid, ref("purchase_order_fifo_neg2")).picking_ids[0] picking_obj.do_partial(context=context) - The price of the product should have changed back to 65.0 - !python {model: product.product}: | - quant_obj = self.pool.get("stock.quant") - quants = quant_obj.search(cr, uid, [('product_id','=',ref("product_fifo_negative"))]) - print "Quants:", [(x.qty, x.location_id.name, x.in_date, x.cost, x.id) for x in quant_obj.browse(cr, uid, quants)] assert self.browse(cr, uid, ref("product_fifo_negative")).standard_price == 65.0, "Product price not updated accordingly. %s found instead of 65" %(self.browse(cr, uid, ref("product_fifo_negative")).standard_price,) diff --git a/addons/stock/__openerp__.py b/addons/stock/__openerp__.py index 7e93c8e7839..fa50332baaa 100644 --- a/addons/stock/__openerp__.py +++ b/addons/stock/__openerp__.py @@ -93,6 +93,7 @@ Dashboard / Reports for Warehouse Management will include: 'test/procrule.yml', 'test/shipment.yml', 'test/packing.yml', + 'test/packingneg.yml', ], 'installable': True, 'application': True, diff --git a/addons/stock/stock.py b/addons/stock/stock.py index 72b49c06b99..6089db6606b 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -178,7 +178,8 @@ class stock_quant(osv.osv): 'qty': fields.float('Quantity', required=True, help="Quantity of products in this quant, in the default unit of measure of the product"), 'package_id': fields.many2one('stock.quant.package', string='Package', help="The package containing this quant"), 'packaging_type_id': fields.related('package_id', 'packaging_id', type='many2one', relation='product.packaging', string='Type of packaging', store=True), - 'reservation_id': fields.many2one('stock.move', 'Reserved for Move', help="Is this quant reserved for a stock.move?"), + 'reservation_id': fields.many2one('stock.move', 'Reserved for Move', help="The move the quant is reserved for"), + 'reservation_op_id': fields.many2one('stock.pack.operation', 'Reserved for Pack Operation', help="The operation the quant is reserved for"), 'lot_id': fields.many2one('stock.production.lot', 'Lot'), 'cost': fields.float('Unit Cost'), 'owner_id': fields.many2one('res.partner', 'Owner', help="This is the owner of the quant"), @@ -213,26 +214,27 @@ class stock_quant(osv.osv): return self.write(cr, uid, toreserve, {'reservation_id': move.id}, context=context) # add location_dest_id in parameters (False=use the destination of the move) - def quants_move(self, cr, uid, quants, move, context=None): + def quants_move(self, cr, uid, quants, move, lot_id = False, owner_id = False, package_id = False, context=None): for quant, qty in quants: #quant may be a browse record or None - quant_record = self.move_single_quant(cr, uid, quant, qty, move, context=context) + quant_record = self.move_single_quant(cr, uid, quant, qty, move, lot_id = lot_id, package_id = package_id, context=context) #quant_record is the quant newly created or already split self._quant_reconcile_negative(cr, uid, quant_record, context=context) + def check_preferred_location(self, cr, uid, move, context=None): return move.location_dest_id - def move_single_quant(self, cr, uid, quant, qty, move, context=None): + def move_single_quant(self, cr, uid, quant, qty, move, lot_id = False, owner_id = False, package_id = False, context=None): if not quant: - quant = self._quant_create(cr, uid, qty, move, context=context) + quant = self._quant_create(cr, uid, qty, move, lot_id = lot_id, owner_id = owner_id, package_id = package_id, context = context) else: self._quant_split(cr, uid, quant, qty, context=context) # FP Note: improve this using preferred locations location_to = self.check_preferred_location(cr, uid, move, context=context) self.write(cr, uid, [quant.id], { 'location_id': location_to.id, - 'reservation_id': move.move_dest_id and move.move_dest_id.id or False, + #'reservation_id': move.move_dest_id and move.move_dest_id.id or False, 'history_ids': [(4, move.id)] }) quant.refresh() @@ -267,9 +269,8 @@ class stock_quant(osv.osv): # # Create a quant in the destination location # Create a negative quant in the source location if it's an internal location - # Reconcile a positive quant with a negative is possible # - def _quant_create(self, cr, uid, qty, move, lot_id=False, owner_id=False, force_location=False, context=None): + def _quant_create(self, cr, uid, qty, move, lot_id=False, owner_id=False, package_id = False, force_location=False, context=None): # FP Note: TODO: compute the right price according to the move, with currency convert # QTY is normally already converted to main product's UoM if context is None: @@ -296,6 +297,7 @@ class stock_quant(osv.osv): negative_vals['qty'] = -qty negative_vals['cost'] = price_unit negative_vals['negative_dest_location_id'] = move.location_dest_id.id + negative_vals['package_id'] = package_id negative_quant_id = self.create(cr, uid, negative_vals, context=context) vals.update({'propagated_from_id': negative_quant_id}) @@ -402,7 +404,7 @@ class stock_quant(osv.osv): #cr.execute('update stock_quant set reservation_id=NULL where reservation_id=%s', (move.id,)) #need write for related store of remaining qty related_quants = [x.id for x in move.reserved_quant_ids] - self.write(cr, uid, related_quants, {'reservation_id': False}, context=context) + self.write(cr, uid, related_quants, {'reservation_id': False, 'reservation_op_id': False}, context=context) return True # @@ -832,6 +834,39 @@ class stock_picking(osv.osv): Needed for parameter create ''' self.rereserve(cr, uid, picking_ids, context=context) + + + + + + def _reserve_quants_ops_move(self, cr, uid, ops, move, qty, create=False, context=None): + """ + Will return the quantity that got reserved + """ + quant_obj = self.pool.get("stock.quant") + op_obj = self.pool.get("stock.pack.operation") + if create and move.location_id.usage != 'internal': + # Create quants + quant = quant_obj._quant_create(cr, uid, qty, move, lot_id = ops.lot_id and ops.lot_id.id or False, owner_id = ops.owner_id and ops.owner_id.id or False, context=context) + #TODO: location_id -> force location? + quant.write({'reservation_op_id': ops.id, 'location_id': move.location_id.id}) + quant_obj.quants_reserve(cr, uid, [(quant, qty)], move, context=context) + return 0 + else: + #Quants get + prefered_order = "reservation_id IS NOT NULL" + dom = op_obj._get_domain(cr, uid, ops, context=context) + dom = dom + [('reservation_id', 'not in', [x.id for x in move.picking_id.move_lines])] + quants = quant_obj.quants_get(cr, uid, move.location_id, move.product_id, qty, domain=dom, prefered_order=prefered_order, context=context) + res_qty = qty + for quant in quants: + if quant[0]: #If quant can be reserved + res_qty -= quant[1] + quant_obj.quants_reserve(cr, uid, quants, move, context=context) + quant_obj.write(cr, uid, [x[0].id for x in quants if x[0]], {'reservation_op_id': ops.id}, context=context) + return res_qty + + # # TODO:rereserve should be improved for giving negative quants when a certain lot is not there @@ -844,7 +879,7 @@ class stock_picking(osv.osv): :return: Tuple (res, res2) res: dictionary of ops with quantity that could not be processed matching ops and moves res2: dictionary of moves with quantity that could not be processed - resneg: the negative quants that should be created with ops and move (TODO) + resneg: the negative quants to be created: resneg[move][ops] gives negative quant to be created (TODO:) tuple of dictionary with quantities of quant operation and product that can not be matched between ops and moves and dictionary with remaining values on moves @@ -852,13 +887,21 @@ class stock_picking(osv.osv): quant_obj = self.pool.get("stock.quant") move_obj = self.pool.get("stock.move") op_obj = self.pool.get("stock.pack.operation") + pack_obj = self.pool.get("stock.quant.package") res = {} # Qty still to do from ops res2 = {} #what is left from moves + resneg= {} #Number of negative quants to create for move : op for picking in self.browse(cr, uid, picking_ids, context=context): + products_moves = {} # unreserve everything and initialize res2 for move in picking.move_lines: quant_obj.quants_unreserve(cr, uid, move, context=context) res2[move.id] = move.product_qty + resneg[move.id] = {} + if move.state == 'assigned': + products_moves.setdefault(move.product_id.id, []).append(move) + + # Resort pack_operation_ids orderedpackops = picking.pack_operation_ids @@ -869,68 +912,43 @@ class stock_picking(osv.osv): #Find moves that correspond if ops.product_id: #TODO: Should have order such that things with lots and packings are searched first - move_ids = move_obj.search(cr, uid, [('picking_id','=',picking.id), ('product_id', '=', ops.product_id.id), ('remaining_qty', '>', 0.0), ('state', '=', 'assigned')], context=context) + move_ids = ops.product_id.id in products_moves and filter(lambda x: res2[x.id] > 0, products_moves[ops.product_id.id]) or [] qty_to_do = ops.product_qty while qty_to_do > 0 and move_ids: - move = move_obj.browse(cr, uid, move_ids.pop(), context=context) - if move.remaining_qty > qty_to_do: + move = move_ids.pop() + if res2[move.id] > qty_to_do: qty = qty_to_do qty_to_do = 0 else: - qty = move.remaining_qty - qty_to_do -= move.remaining_qty - - if create and move.location_id.usage != 'internal': - # Create quants - vals = { - 'product_id': move.product_id.id, - 'location_id': move.location_id.id, - 'qty': qty, - #'cost': price_unit, - 'history_ids': [(4, move.id)], - 'in_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), - 'company_id': move.company_id.id, - 'lot_id': ops.lot_id and ops.lot_id.id or False, - 'owner_id': ops.owner_id and ops.owner_id.id or False, - 'reservation_id': move.id, #Reserve at once - 'package_id': ops.result_package_id and ops.result_package_id.id or False, - } - quant_id = quant_obj.create(cr, uid, vals, context=context) - else: - #Quants get - prefered_order = "reservation_id IS NOT NULL" - dom = op_obj._get_domain(cr, uid, ops, context=context) - dom = dom + [('reservation_id', 'not in', [x.id for x in picking.move_lines])] - quants = quant_obj.quants_get(cr, uid, move.location_id, move.product_id, qty, domain=dom, prefered_order=prefered_order, context=context) - quant_obj.quants_reserve(cr, uid, quants, move, context=context) - #In the end, move quants in correct package - if create: - quant_obj.write(cr, uid, [x[0].id for x in quants if x[0] != None], {'package_id': ops.result_package_id and ops.result_package_id.id or False}, context=context) + qty = res2[move.id] + qty_to_do -= res2[move.id] + neg_qty = self._reserve_quants_ops_move(cr, uid, ops, move, qty, create=create, context=context) + if neg_qty > 0: + resneg[move.id].setdefault(ops.id, 0) + resneg [move.id][ops.id] += neg_qty res2[move.id] -= qty res[ops.id] = {} res[ops.id][ops.product_id.id] = qty_to_do elif ops.package_id: - quants = ops.package_id.quant_ids #_get_package_lines - + quants = quant_obj.browse(cr, uid, pack_obj.get_content(cr, uid, [ops.package_id.id], context=context)) + quants = [x for x in quants if x.qty > 0] #Negative quants should not be moved for quant in quants: - move_ids = move_obj.search(cr, uid, [('picking_id', '=', picking.id), ('product_id', '=', quant.product_id.id), ('remaining_qty', '>', 0.0)]) + move_ids = quant.product_id.id in products_moves and filter(lambda x: res2[x.id] > 0, products_moves[quant.product_id.id]) or [] qty_to_do = quant.qty while qty_to_do > 0 and move_ids: - move = move_obj.browse(cr, uid, move_ids.pop(), context=context) - if move.remaining_qty > qty_to_do: + move = move_ids.pop() + if res2[move.id] > qty_to_do: qty = qty_to_do qty_to_do = 0.0 else: - qty = move.remaining_qty - qty_to_do -= move.remaining_qty + qty = res2[move.id] + qty_to_do -= res2[move.id] quant_obj.quants_reserve(cr, uid, [(quant, qty)], move, context=context) + quant_obj.write(cr, uid, [quant.id], {'reservation_op_id': ops.id}, context=context) res2[move.id] -= qty res.setdefault(ops.id, {}).setdefault(quant.product_id.id, 0.0) res[ops.id][quant.product_id.id] += qty_to_do - #Add parent package - if create: - self.pool.get("stock.quant.package").write(cr, uid, [ops.package_id.id], {'parent_id': ops.result_package_id and ops.result_package_id.id or False}, context=context) - return (res, res2) + return (res, res2, resneg) def do_partial(self, cr, uid, picking_ids, context=None): @@ -946,6 +964,7 @@ class stock_picking(osv.osv): else: #First thing that needs to happen is rereserving the quants res = self.rereserve(cr, uid, [picking.id], create = True, context = context) #This time, quants need to be created + resneg = res[2] orig_moves = picking.move_lines orig_qtys = {} for orig in orig_moves: @@ -957,8 +976,7 @@ class stock_picking(osv.osv): product = self.pool.get('product.product').browse(cr, uid, prod, context=context) qty = res[0][ops][prod] if qty > 0: - #TODO: Maybe should try to reserve quants? / Make copy instead of create, but have to put move_dest_id False then e.g. link purchase order line? - quant = False + #Create moves for products too many on operation move_id = stock_move_obj.create(cr, uid, { 'name': product.name, 'product_id': product.id, @@ -967,12 +985,17 @@ class stock_picking(osv.osv): 'location_id': picking.location_id.id, 'location_dest_id': picking.location_dest_id.id, 'picking_id': picking.id, - 'reserved_quant_ids': quant and [(4, quant.id)] or [], 'picking_type_id': picking.picking_type_id.id, 'group_id': picking.group_id.id, }, context=context) + stock_move_obj.action_confirm(cr, uid, [move_id], context=context) + move = stock_move_obj.browse(cr, uid, move_id, context=context) + ops_rec = self.pool.get("stock.pack.operation").browse(cr, uid, ops, context=context) + resneg[move_id] = {} + resneg[move_id][ops] = self._reserve_quants_ops_move(cr, uid, ops_rec, move, qty, create=True, context=context) extra_moves.append(move_id) res2 = res[1] + #Backorder for move in res2.keys(): if res2[move] > 0: mov = stock_move_obj.browse(cr, uid, move, context=context) @@ -983,13 +1006,12 @@ class stock_picking(osv.osv): orig_moves = [x for x in orig_moves if res[1][x.id] < orig_qtys[x.id]] for move in orig_moves + stock_move_obj.browse(cr, uid, extra_moves, context=context): if move.state == 'draft': - self.pool.get('stock.move').action_confirm(cr, uid, [move.id], - context=context) + self.pool.get('stock.move').action_confirm(cr, uid, [move.id], context=context) todo.append(move.id) elif move.state in ('assigned','confirmed'): todo.append(move.id) if len(todo): - self.pool.get('stock.move').action_done(cr, uid, todo, context=context) + self.pool.get('stock.move').action_done(cr, uid, todo, negatives = resneg, context=context) picking.refresh() self._create_backorder(cr, uid, picking, context=context) return True @@ -1634,7 +1656,7 @@ class stock_move(osv.osv): # res[move.id] = [x.id for x in move.reserved_quant_ids] # return res - def action_done(self, cr, uid, ids, context=None): + def action_done(self, cr, uid, ids, negatives = False, context=None): """ Makes the move done and if all moves are done, it will finish the picking. If quants are not assigned yet, it should assign them Putaway strategies should be applied @@ -1642,13 +1664,20 @@ class stock_move(osv.osv): """ context = context or {} quant_obj = self.pool.get("stock.quant") - + ops_obj = self.pool.get("stock.pack.operation") + pack_obj = self.pool.get("stock.quant.package") todo = [move.id for move in self.browse(cr, uid, ids, context=context) if move.state == "draft"] if todo: self.action_confirm(cr, uid, todo, context=context) pickings = set() for move in self.browse(cr, uid, ids, context=context): +# if negatives and negatives[move.id]: +# for ops in negatives[move.id].keys(): +# quants_to_move = [(None, negatives[move.id, x) for x in negatives] +# quant_obj.quants_move(cr, uid, quants_to_move, move, context=context) + + if move.picking_id: pickings.add(move.picking_id.id) qty = move.product_qty @@ -1657,17 +1686,33 @@ 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), ('qty', '>', 0)] + dom = ['|', ('reservation_id', '=', False), ('reservation_id', '=', move.id), ('qty', '>', 0)] prefered_order = 'reservation_id' # if lot_id: # prefered_order = 'lot_id<>' + lot_id + ", " + prefered_order # if pack_id: # prefered_order = 'pack_id<>' + pack_id + ", " + prefered_order - quants = quant_obj.quants_get(cr, uid, move.location_id, move.product_id, qty, domain=domain, prefered_order = prefered_order, context=context) - #Will move all quants_get and as such create negative quants - quant_obj.quants_move(cr, uid, quants, move, context=context) + if move.picking_id and move.picking_id.pack_operation_ids: + quants = quant_obj.quants_get(cr, uid, move.location_id, move.product_id, qty - move.remaining_qty, domain=dom, prefered_order = prefered_order, context=context) + quant_obj.quants_move(cr, uid, quants, move, context=context) + for negative_op in negatives[move.id].keys(): + ops = ops_obj.browse(cr, uid, negative_op, context=context) + negatives[move.id][negative_op] = quant_obj.quants_move(cr, uid, [(None, negatives[move.id][negative_op])], move, + lot_id = ops.lot_id and ops.lot_id.id or False, + owner_id = ops.owner_id and ops.owner_id.id or False, + package_id = ops.package_id and ops.package_id.id or False, context=context) + #Packing: + reserved_ops = list(set([x.reservation_op_id.id for x in move.reserved_quant_ids])) + for ops in ops_obj.browse(cr, uid, reserved_ops, context=context): + if ops.product_id: + quant_obj.write(cr, uid, [x.id for x in ops.reserved_quant_ids], {'package_id': ops.result_package_id and ops.result_package_id.id or False}, context=context) + else: + pack_obj.write(cr, uid, [ops.package_id.id], {'parent_id': ops.result_package_id and ops.result_package_id.id or False}, context=context) + else: + quants = quant_obj.quants_get(cr, uid, move.location_id, move.product_id, qty, domain=dom, prefered_order = prefered_order, context=context) + #Will move all quants_get and as such create negative quants + quant_obj.quants_move(cr, uid, quants, move, context=context) quant_obj.quants_unreserve(cr, uid, move, context=context) - # #Check moves that were pushed if move.move_dest_id.state in ('waiting', 'confirmed'): @@ -2280,6 +2325,17 @@ class stock_package(osv.osv): class stock_pack_operation(osv.osv): _name = "stock.pack.operation" _description = "Packing Operation" + + def _get_remaining_qty(self, cr, uid, ids, context=None): + res = {} + for ops in self.browse(cr, uid, ids, context=context): + qty = ops.product_qty + for quant in ops.reserved_quant_ids: + qty -= quant.qty + res[ops.id] = qty + return res + + _columns = { 'picking_id': fields.many2one('stock.picking', 'Stock Picking', help='The stock operation where the packing has been made', required=True), 'product_id': fields.many2one('product.product', 'Product', ondelete="CASCADE"), # 1 @@ -2294,6 +2350,8 @@ class stock_pack_operation(osv.osv): #'update_cost': fields.boolean('Need cost update'), 'cost': fields.float("Cost", help="Unit Cost for this product line"), 'currency': fields.many2one('res.currency', string="Currency", help="Currency in which Unit cost is expressed", ondelete='CASCADE'), + 'reserved_quant_ids': fields.one2many('stock.quant', 'reservation_op_id', string='Reserved Quants', readonly=True, help='Quants reserved for this operation'), + 'remaining_qty': fields.function(_get_remaining_qty, type='float', string='Remaining Qty'), } _defaults = { diff --git a/addons/stock/test/packing.yml b/addons/stock/test/packing.yml index 451c2ac885f..d0edef15f7a 100644 --- a/addons/stock/test/packing.yml +++ b/addons/stock/test/packing.yml @@ -85,7 +85,7 @@ backorder = self.search(cr, uid, [('backorder_id', '=', ref('pick1'))]) assert not backorder, "" #Check extra moves created - assert len(picking.move_lines) == 1, "" + assert len(picking.move_lines) == 1, "" - Make a delivery order of 300 pieces to the customer - @@ -114,9 +114,9 @@ for rec in delivery_id.pack_operation_ids: if rec.package_id.name == 'Pallet 2': lot_ids = self.pool.get("stock.production.lot").search(cr, uid, [('product_id', '=', ref('product1')), ('name','=','Lot A')]) - stock_pack.write(cr, uid, rec.id, {'product_id': ref('product1'), 'product_qty': 20, 'lot_id': lot_ids[0]}, context=context) + stock_pack.write(cr, uid, [rec.id], {'product_id': ref('product1'), 'product_qty': 20, 'lot_id': lot_ids[0]}, context=context) if rec.package_id.name == 'Pallet 3': - stock_pack.write(cr, uid, rec.id, {'product_id': ref('product1'),'product_qty': 10}, context=context) + stock_pack.write(cr, uid, [rec.id], {'product_id': ref('product1'),'product_qty': 10}, context=context) - Process this picking - diff --git a/addons/stock/test/packingneg.yml b/addons/stock/test/packingneg.yml new file mode 100644 index 00000000000..500e580e90e --- /dev/null +++ b/addons/stock/test/packingneg.yml @@ -0,0 +1,113 @@ +- + Create a new "negative" stockable product +- + !record {model: product.product, id: product_neg}: + name: Negative product + type: product + categ_id: product.product_category_1 + list_price: 100.0 + standard_price: 70.0 + seller_ids: + - delay: 1 + name: base.res_partner_2 + min_qty: 2.0 + qty: 5.0 + uom_id: product.product_uom_unit + uom_po_id: product.product_uom_unit +- + Create an incoming picking for this product of 300 PCE from suppliers to stock +- + !record {model: stock.picking, id: pick_neg}: + name: Incoming picking + partner_id: base.res_partner_2 + picking_type_id: picking_type_in + move_lines: + - product_id: product_neg + product_uom_qty: 300.00 + location_id: stock_location_suppliers + location_dest_id: stock_location_stock +- + Confirm and assign picking and prepare partial +- + !python {model: stock.picking}: | + self.action_confirm(cr, uid, [ref('pick_neg')], context=context) + self.do_prepare_partial(cr, uid, [ref('pick_neg')], context=context) +- + Put 120 pieces on Palneg 1 (package), 120 pieces on Palneg 2 with lot A and 60 pieces on Palneg 3 +- + !python {model: stock.picking}: | + #Change quantity of first to 120 and create 2 others quant operations + record = self.browse(cr, uid, ref('pick_neg'), context=context) + stock_pack = self.pool.get('stock.pack.operation') + stock_quant_pack = self.pool.get('stock.quant.package') + #create lot A + lot_a = self.pool.get('stock.production.lot').create(cr, uid, {'name': 'Lot neg', 'product_id': ref('product_neg')}, context=context) + #create package + package1 = stock_quant_pack.create(cr, uid, {'name': 'Palneg 1'}, context=context) + package2 = stock_quant_pack.create(cr, uid, {'name': 'Palneg 2'}, context=context) + package3 = stock_quant_pack.create(cr, uid, {'name': 'Palneg 3'}, context=context) + #Create package for each line and assign it as result_package_id + #create pack operation + stock_pack.write(cr, uid, record.pack_operation_ids[0].id, {'result_package_id': package1, 'product_qty': 120}) + new_pack1 = stock_pack.create(cr, uid, {'product_id': ref('product_neg'), 'product_uom_id': ref('product.product_uom_unit'), 'picking_id': ref('pick_neg'), 'lot_id': lot_a, 'result_package_id': package2, 'product_qty': 120}, context=context) + new_pack2 = stock_pack.create(cr, uid, {'product_id': ref('product_neg'), 'product_uom_id': ref('product.product_uom_unit'), 'picking_id': ref('pick_neg'), 'result_package_id': package3, 'product_qty': 60}, context=context) +- + Transfer the reception +- + !python {model: stock.picking}: | + self.do_partial(cr, uid, [ref('pick_neg')], context=context) +- + Make a delivery order of 300 pieces to the customer +- + !record {model: stock.picking, id: delivery_order_neg}: + name: outgoing picking + partner_id: base.res_partner_4 + picking_type_id: stock.picking_type_out + move_lines: + - product_id: product_neg + product_uom_qty: 300.00 + location_id: stock_location_stock + location_dest_id: stock_location_customers +- + Assign and confirm +- + !python {model: stock.picking}: | + self.action_confirm(cr, uid, [ref('delivery_order_neg')], context=context) + self.action_assign(cr, uid, [ref('delivery_order_neg')]) +- + Instead of doing the 300 pieces, you decide to take pallet 1 (do not mention product in operation here) and 140 pieces from lot A/pallet 2 and 10 pieces from pallet 3 +- + !python {model: stock.picking}: | + stock_pack = self.pool.get('stock.pack.operation') + self.do_prepare_partial(cr, uid, [ref('delivery_order_neg')], context=context) + delivery_id = self.browse(cr, uid, ref('delivery_order_neg'), context=context) + for rec in delivery_id.pack_operation_ids: + if rec.package_id.name == 'Palneg 2': + lot_ids = self.pool.get("stock.production.lot").search(cr, uid, [('product_id', '=', ref('product_neg')), ('name','=','Lot neg')]) + stock_pack.write(cr, uid, [rec.id], {'product_id': ref('product_neg'), 'product_qty': 140, 'lot_id': lot_ids[0]}, context=context) + if rec.package_id.name == 'Palneg 3': + stock_pack.write(cr, uid, [rec.id], {'product_id': ref('product_neg'),'product_qty': 10}, context=context) +- + Process this picking +- + !python {model: stock.picking}: | + self.do_partial(cr, uid, [ref('delivery_order_neg')], context=context) +- + Check the quants that you have 120 pieces pallet 1 in customers, 100 pieces pallet 2 in stock and 20 with customers and 50 in stock, 10 in customers from pallet 3 +- + !python {model: stock.quant}: | + reco_id = self.search(cr ,uid , [('product_id','=',ref('product_neg'))], context=context) + for rec in self.browse(cr, uid, reco_id, context=context): + if rec.package_id.name == 'Palneg 1' and rec.location_id.id == ref('stock_location_customers'): + assert rec.qty == 120, "Should have 120 pieces on pallet 1" + elif rec.package_id.name == 'Palneg 2' and rec.location_id.id == ref('stock_location_stock'): + assert rec.qty == -20, "Should have -20 pieces in stock on pallet 2" + assert rec.lot_id.name == 'Lot neg', "It should have kept its Lot" + elif rec.lot_id.name == 'Lot neg' and rec.location_id.id == ref('stock_location_customers'): + assert ((rec.qty == 20 or rec.qty == 120) and not rec.package_id), "Should have 140 pieces (120+20) in customer location from pallet 2 and lot A" + elif rec.package_id.name == 'Palneg 3' and rec.location_id.id == ref('stock_location_stock'): + assert rec.qty == 30 or rec.qty == 20, "Should have 30 and 20 pieces in stock on pallet 3" + elif not rec.package_id and not rec.lot_id and rec.location_id.id == ref('stock_location_customers'): + assert rec.qty == 10, "Should have 10 pieces in customer location from pallet 3" + else: + assert False, "Unrecognized quant" \ No newline at end of file diff --git a/addons/stock_account/stock_account.py b/addons/stock_account/stock_account.py index e520901ffe4..7fd892f10b2 100644 --- a/addons/stock_account/stock_account.py +++ b/addons/stock_account/stock_account.py @@ -124,11 +124,13 @@ class stock_quant(osv.osv): else: self._create_account_move_line(cr, uid, quant, move, acc_valuation, acc_dest, journal_id, context=ctx) - def move_single_quant(self, cr, uid, quant, qty, move, context=None): - quant_record = super(stock_quant, self).move_single_quant(cr, uid, quant, qty, move, context=context) + + def move_single_quant(self, cr, uid, quant, qty, move, lot_id=False, owner_id=False, package_id= False, context=None): + quant_record = super(stock_quant, self).move_single_quant(cr, uid, quant, qty, move, lot_id = lot_id, owner_id = owner_id, package_id = package_id, context=context) self._account_entry_move(cr, uid, quant_record, move, context=context) return quant_record + 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 @@ -223,9 +225,9 @@ class stock_quant(osv.osv): class stock_move(osv.osv): _inherit = "stock.move" - def action_done(self, cr, uid, ids, context=None): + def action_done(self, cr, uid, ids, negatives = False, context=None): self.product_price_update_before_done(cr, uid, ids, context=context) - super(stock_move, self).action_done(cr, uid, ids, context=context) + super(stock_move, self).action_done(cr, uid, ids, negatives=negatives, context=context) self.product_price_update_after_done(cr, uid, ids, context=context) def _store_average_cost_price(self, cr, uid, move, context=None): diff --git a/addons/stock_complex_routes/test/dropship.yml b/addons/stock_complex_routes/test/dropship.yml index 11a26cc1028..37e6670f6ca 100644 --- a/addons/stock_complex_routes/test/dropship.yml +++ b/addons/stock_complex_routes/test/dropship.yml @@ -68,20 +68,13 @@ - !python {model: stock.picking}: | po_id = self.pool.get('purchase.order').search(cr, uid, [('partner_id', '=', ref('supplier_dropship'))]) - print "Picking_ids", self.pool.get("purchase.order").browse(cr, uid, po_id[0]).picking_ids picking_id = self.search(cr, uid, [('purchase_id', '=', po_id[0])]) - print "Pick_id", picking_id - print [(x.move_lines[0].product_id.name, x.move_lines[0].product_uom_qty) for x in self.browse(cr, uid, picking_id)] self.do_partial(cr, uid, picking_id) - Check one quant was created in Customers location with 200 pieces and one move in the history_ids - !python {model: stock.quant}: | - quant_obj = self.pool.get("stock.quant") - quant_ids = self.search(cr, uid, [('product_id', '=', ref("drop_shop_product"))]) - print "Quants:", [(x.qty, x.location_id.name, x.in_date, x.cost) for x in quant_obj.browse(cr, uid, quant_ids)] quant_ids = self.search(cr, uid, [('location_id', '=', ref('stock.stock_location_customers')),('qty', '=', 200), ('product_id', '=', ref("drop_shop_product"))]) - print "Quants", quant_ids assert quant_ids, 'No Quant found' assert len(quant_ids) == 1 assert len(self.browse(cr, uid, quant_ids)[0].history_ids) == 1