diff --git a/addons/purchase/test/fifo_returns.yml b/addons/purchase/test/fifo_returns.yml index 1dc54d5eb13..50cddf25675 100644 --- a/addons/purchase/test/fifo_returns.yml +++ b/addons/purchase/test/fifo_returns.yml @@ -74,7 +74,7 @@ return_picking_id, dummy = self._create_returns(cr, uid, [return_id], context={'active_id': pick_ids[0].id}) self.pool.get('stock.picking').do_transfer(cr, uid, [return_picking_id]) - - Check the standard price of the product changed to 70.0 (as returns to supplier are processed as normal outgoing shipment we have sent 10kg at 50€ and 20kg at 80€, because of FIFO, so the average to store on the product is 70€) + Check the standard price of the product changed to 80.0 as we returned the quants of purchase order 2 - !python {model: product.product}: | - assert self.browse(cr, uid, ref("product_fiforet_icecream")).standard_price == 70.0, 'Standard price should have changed to 70.0! %s found instead' % (self.browse(cr, uid, ref("product_fiforet_icecream")).standard_price,) + assert self.browse(cr, uid, ref("product_fiforet_icecream")).standard_price == 80.0, 'Standard price should have changed to 80.0! %s found instead' % (self.browse(cr, uid, ref("product_fiforet_icecream")).standard_price,) diff --git a/addons/stock/stock.py b/addons/stock/stock.py index b4036226dd0..78c82a79ded 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -26,7 +26,6 @@ import time from openerp.osv import fields, osv from openerp.tools.translate import _ -from openerp import tools from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT, DEFAULT_SERVER_DATE_FORMAT from openerp import SUPERUSER_ID import openerp.addons.decimal_precision as dp @@ -787,8 +786,8 @@ class stock_picking(osv.osv): '''This function prints the delivery order''' assert len(ids) == 1, 'This option should only be used for a single id at a time' datas = { - 'model': 'stock.picking', - 'ids': ids, + 'model': 'stock.picking', + 'ids': ids, } return {'type': 'ir.actions.report.xml', 'report_name': 'stock.picking.list', 'datas': datas, 'nodestroy': True} @@ -796,8 +795,8 @@ class stock_picking(osv.osv): '''This function prints the picking list''' assert len(ids) == 1, 'This option should only be used for a single id at a time' datas = { - 'model': 'stock.picking', - 'ids': ids, + 'model': 'stock.picking', + 'ids': ids, } return {'type': 'ir.actions.report.xml', 'report_name': 'stock.picking.list.internal', 'datas': datas, 'nodestroy': True} @@ -844,15 +843,6 @@ class stock_picking(osv.osv): self.do_prepare_partial(cr, uid, [pick.id], context=None) return True - def cancel_assign(self, cr, uid, ids, context=None): - """ Cancels picking for the moves that are in the assigned state - @return: True - """ - for pick in self.browse(cr, uid, ids, context=context): - move_ids = [x.id for x in pick.move_lines if x not in ['done', 'cancel']] - self.pool.get('stock.move').cancel_assign(cr, uid, move_ids, context=context) - return True - def action_cancel(self, cr, uid, ids, context=None): for pick in self.browse(cr, uid, ids, context=context): ids2 = [move.id for move in pick.move_lines] @@ -1006,11 +996,11 @@ class stock_picking(osv.osv): moves_to_unreserve = [] pack_line_to_unreserve = [] for picking in self.browse(cr, uid, picking_ids, context=context): - moves_to_unreserve += [m.id for m in picking.move_lines] + moves_to_unreserve += [m.id for m in picking.move_lines if m.state not in ('done', 'cancel')] pack_line_to_unreserve += [p.id for p in picking.pack_operation_ids] - if pack_line_to_unreserve: - self.pool.get('stock.pack.operation').unlink(cr, uid, pack_line_to_unreserve, context=context) if moves_to_unreserve: + if pack_line_to_unreserve: + self.pool.get('stock.pack.operation').unlink(cr, uid, pack_line_to_unreserve, context=context) self.pool.get('stock.move').do_unreserve(cr, uid, moves_to_unreserve, context=context) def do_recompute_remaining_quantities(self, cr, uid, picking_ids, context=None): @@ -1215,14 +1205,13 @@ class stock_production_lot(osv.osv): @return: A dictionary of values """ quant_obj = self.pool.get("stock.quant") - move_obj = self.pool.get("stock.move") quants = quant_obj.search(cr, uid, [('lot_id', 'in', ids)], context=context) moves = set() for quant in quant_obj.browse(cr, uid, quants, context=context): moves |= {move.id for move in quant.history_ids} if moves: - return { - 'domain': "[('id','in',["+','.join(map(str, list(moves)))+"])]", + return { + 'domain': "[('id','in',[" + ','.join(map(str, list(moves))) + "])]", 'name': _('Traceability'), 'view_mode': 'tree,form', 'view_type': 'form', @@ -1231,7 +1220,6 @@ class stock_production_lot(osv.osv): 'type': 'ir.actions.act_window', } return False - # ---------------------------------------------------- @@ -1407,6 +1395,7 @@ class stock_move(osv.osv): 'price_unit': fields.float('Unit Price', help="Technical field used to record the product cost set by the user during a picking confirmation (when costing method used is 'average price' or 'real'). Value given in company currency and in product uom."), # as it's a technical field, we intentionally don't provide the digits attribute 'company_id': fields.many2one('res.company', 'Company', required=True, select=True), + 'split_from': fields.many2one('stock.move', string="Move Split From", help="Technical field used to track the origin of a split move, which can be useful in case of debug"), 'backorder_id': fields.related('picking_id', 'backorder_id', type='many2one', relation="stock.picking", string="Back Order of", select=True), 'origin': fields.char("Source"), 'procure_method': fields.selection([('make_to_stock', 'Make to Stock'), ('make_to_order', 'Make to Order')], 'Procurement Method', required=True, help="Make to Stock: When needed, the product is taken from the stock or we wait for replenishment. \nMake to Order: When needed, the product is purchased or produced."), @@ -1493,13 +1482,16 @@ class stock_move(osv.osv): default['reserved_quant_ids'] = [] default['returned_move_ids'] = [] default['linked_move_operation_ids'] = [] - default['origin_returned_move_id'] = False + if not default.get('origin_returned_move_id'): + default['origin_returned_move_id'] = False default['state'] = 'draft' return super(stock_move, self).copy_data(cr, uid, id, default, context) def do_unreserve(self, cr, uid, move_ids, context=None): quant_obj = self.pool.get("stock.quant") for move in self.browse(cr, uid, move_ids, context=context): + if move.state in ('done', 'cancel'): + raise osv.except_osv(_('Operation Forbidden!'), _('Cannot unreserve a done move')) quant_obj.quants_unreserve(cr, uid, move, context=context) putaway_values = [] for putaway_rec in move.putaway_ids: @@ -1534,7 +1526,10 @@ class stock_move(osv.osv): def _push_apply(self, cr, uid, moves, context=None): push_obj = self.pool.get("stock.location.path") for move in moves: - if not move.move_dest_id: + #1) if the move is already chained, there is no need to check push rules + #2) if the move is a returned move, we don't want to check push rules, as returning a returned move is the only decent way + # to receive goods without triggering the push rules again (which would duplicate chained operations) + if not move.move_dest_id and not move.origin_returned_move_id: domain = [('location_from_id', '=', move.location_dest_id.id)] if move.warehouse_id: domain += ['|', ('warehouse_id', '=', move.warehouse_id.id), ('warehouse_id', '=', False)] @@ -1812,12 +1807,6 @@ class stock_move(osv.osv): self._putaway_check(cr, uid, ids, context=context) return self.write(cr, uid, ids, {'state': 'assigned'}) - def cancel_assign(self, cr, uid, ids, context=None): - """ Changes the state to confirmed. - @return: True - """ - return self.write(cr, uid, ids, {'state': 'confirmed'}) - def check_tracking(self, cr, uid, move, lot_id, context=None): """ Checks if serial number is assigned to stock move or not and raise an error if it had to. """ @@ -1853,6 +1842,9 @@ class stock_move(osv.osv): fallback_domain = prev_quant_ids and [('id', 'not in', prev_quant_ids)] or [] #we always keep the quants already assigned and try to find the remaining quantity on quants not assigned only main_domain = [('reservation_id', '=', False), ('qty', '>', 0)] + #if the move is returned from another, restrict the choice of quants to the ones that follow the returned move + if move.origin_returned_move_id: + main_domain += [('history_ids', 'in', move.origin_returned_move_id.id)] #first try to find quants based on specific domains given by linked operations for record in move.linked_move_operation_ids: domain = main_domain + self.pool.get('stock.move.operation.link').get_specific_domain(cr, uid, record, context=context) @@ -2043,7 +2035,8 @@ class stock_move(osv.osv): 'move_dest_id': False, 'reserved_quant_ids': [], 'restrict_lot_id': restrict_lot_id, - 'restrict_partner_id': restrict_partner_id + 'restrict_partner_id': restrict_partner_id, + 'split_from': move.id, } if context.get('source_location_id'): defaults['location_id'] = context['source_location_id'] diff --git a/addons/stock/stock_view.xml b/addons/stock/stock_view.xml index fba735de513..0afe8f8df34 100644 --- a/addons/stock/stock_view.xml +++ b/addons/stock/stock_view.xml @@ -859,6 +859,7 @@ + diff --git a/addons/stock/wizard/stock_return_picking.py b/addons/stock/wizard/stock_return_picking.py index c16c0d51230..112b233bf1f 100644 --- a/addons/stock/wizard/stock_return_picking.py +++ b/addons/stock/wizard/stock_return_picking.py @@ -19,8 +19,6 @@ # ############################################################################## -import time - from openerp.osv import osv, fields from openerp.tools.translate import _ import openerp.addons.decimal_precision as dp @@ -34,7 +32,7 @@ class stock_return_picking_line(osv.osv_memory): 'quantity': fields.float("Quantity", digits_compute=dp.get_precision('Product Unit of Measure'), required=True), 'wizard_id': fields.many2one('stock.return.picking', string="Wizard"), 'move_id': fields.many2one('stock.move', "Move"), - 'lot_id': fields.related('move_id', 'quant_ids', 'lot_id', type="many2one", relation='stock.production.lot', string='Serial Number', readonly=True), + 'lot_id': fields.many2one('stock.production.lot', 'Serial Number', help="Used to choose the lot/serial number of the product returned"), } @@ -43,6 +41,7 @@ class stock_return_picking(osv.osv_memory): _description = 'Return Picking' _columns = { 'product_return_moves': fields.one2many('stock.return.picking.line', 'wizard_id', 'Moves'), + 'move_dest_exists': fields.boolean('Chained Move Exists', readonly=True, help="Technical field used to hide help tooltip if not needed"), } def default_get(self, cr, uid, fields, context=None): @@ -60,24 +59,33 @@ class stock_return_picking(osv.osv_memory): context = {} res = super(stock_return_picking, self).default_get(cr, uid, fields, context=context) record_id = context and context.get('active_id', False) or False + uom_obj = self.pool.get('product.uom') pick_obj = self.pool.get('stock.picking') pick = pick_obj.browse(cr, uid, record_id, context=context) + quant_obj = self.pool.get("stock.quant") + chained_move_exist = False if pick: if pick.state != 'done': raise osv.except_osv(_('Warning!'), _("You may only return pickings that are Done!")) - for line in pick.move_lines: - qty = line.product_uom_qty - if line.returned_move_ids: - for returned_move in line.returned_move_ids: - if returned_move.product_id.id == line.product_id.id: - qty -= returned_move.product_uom_qty - if qty > 0: - result1.append({'product_id': line.product_id.id, 'quantity': qty, 'move_id': line.id}) + for move in pick.move_lines: + if move.move_dest_id: + chained_move_exist = True + #Sum the quants in that location that can be returned (they should have been moved by the moves that were included in the returned picking) + qty = 0 + quant_search = quant_obj.search(cr, uid, [('history_ids', 'in', move.id), ('qty', '>', 0.0), ('location_id', 'child_of', move.location_dest_id.id)], context=context) + for quant in quant_obj.browse(cr, uid, quant_search, context=context): + if not quant.reservation_id or quant.reservation_id.origin_returned_move_id.id != move.id: + qty += quant.qty + qty = uom_obj._compute_qty(cr, uid, move.product_id.uom_id.id, qty, move.product_uom.id) + result1.append({'product_id': move.product_id.id, 'quantity': qty, 'move_id': move.id}) + if len(result1) == 0: raise osv.except_osv(_('Warning!'), _("No products to return (only lines in Done state and not fully returned yet can be returned)!")) if 'product_return_moves' in fields: res.update({'product_return_moves': result1}) + if 'move_dest_exists' in fields: + res.update({'move_dest_exists': chained_move_exist}) return res def _create_returns(self, cr, uid, ids, context=None): @@ -92,16 +100,32 @@ class stock_return_picking(osv.osv_memory): data = self.read(cr, uid, ids[0], context=context) returned_lines = 0 + # Cancel assignment of existing chained assigned moves + moves_to_unreserve = [] + for move in pick.move_lines: + to_check_moves = [move.move_dest_id] + while to_check_moves: + current_move = to_check_moves.pop() + if current_move.state not in ('done', 'cancel') and current_move.reserved_quant_ids: + moves_to_unreserve.append(current_move.id) + split_move_ids = move_obj.search(cr, uid, [('split_from', '=', current_move.id)], context=context) + if split_move_ids: + to_check_moves += move_obj.browse(cr, uid, split_move_ids, context=context) + + if moves_to_unreserve: + move_obj.do_unreserve(cr, uid, moves_to_unreserve, context=context) + #break the link between moves in order to be able to fix them later if needed + move_obj.write(cr, uid, moves_to_unreserve, {'move_orig_ids': False}, context=context) + #Create new picking for returned products pick_type_id = pick.picking_type_id.return_picking_type_id and pick.picking_type_id.return_picking_type_id.id or pick.picking_type_id.id - new_picking = pick_obj.copy(cr, uid, pick.id, { 'move_lines': [], 'picking_type_id': pick_type_id, 'state': 'draft', 'origin': pick.name, - },context=context) - + }, context=context) + for data_get in data_obj.browse(cr, uid, data['product_return_moves'], context=context): move = data_get.move_id if not move: @@ -109,9 +133,6 @@ class stock_return_picking(osv.osv_memory): new_qty = data_get.quantity if new_qty: returned_lines += 1 - quant_ids = [] - for quant in move.quant_ids: - quant_ids.append((4, quant.id)) move_obj.copy(cr, uid, move.id, { 'product_id': data_get.product_id.id, 'product_uom_qty': new_qty, @@ -120,9 +141,11 @@ class stock_return_picking(osv.osv_memory): 'state': 'draft', 'location_id': move.location_dest_id.id, 'location_dest_id': move.location_id.id, - 'reserved_quant_ids': quant_ids, 'origin_returned_move_id': move.id, + 'procure_method': 'make_to_stock', + 'restrict_lot_id': data_get.lot_id.id, }) + if not returned_lines: raise osv.except_osv(_('Warning!'), _("Please specify at least one non-zero quantity.")) @@ -143,13 +166,13 @@ class stock_return_picking(osv.osv_memory): new_picking_id, pick_type_id = self._create_returns(cr, uid, ids, context=context) # Override the context to disable all the potential filters that could have been set previously ctx = { - 'search_default_picking_type_id': pick_type_id, - 'search_default_draft': False, - 'search_default_assigned': False, - 'search_default_confirmed': False, - 'search_default_ready': False, - 'search_default_late': False, - 'search_default_available': False, + 'search_default_picking_type_id': pick_type_id, + 'search_default_draft': False, + 'search_default_assigned': False, + 'search_default_confirmed': False, + 'search_default_ready': False, + 'search_default_late': False, + 'search_default_available': False, } return { 'domain': "[('id', 'in', [" + str(new_picking_id) + "])]", diff --git a/addons/stock/wizard/stock_return_picking_view.xml b/addons/stock/wizard/stock_return_picking_view.xml index 49b0c9a0e5c..b26918746a1 100644 --- a/addons/stock/wizard/stock_return_picking_view.xml +++ b/addons/stock/wizard/stock_return_picking_view.xml @@ -41,9 +41,18 @@ stock.return.picking
-