[IMP] When preparing pack operations, use the smallest UoM of the moves if they are...
[odoo/odoo.git] / addons / stock / stock.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 from datetime import date, datetime
23 from dateutil import relativedelta
24 import json
25 import time
26
27 from openerp.osv import fields, osv
28 from openerp.tools.float_utils import float_compare, float_round
29 from openerp.tools.translate import _
30 from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT, DEFAULT_SERVER_DATE_FORMAT
31 from openerp import SUPERUSER_ID, api
32 import openerp.addons.decimal_precision as dp
33 from openerp.addons.procurement import procurement
34 import logging
35
36
37 _logger = logging.getLogger(__name__)
38 #----------------------------------------------------------
39 # Incoterms
40 #----------------------------------------------------------
41 class stock_incoterms(osv.osv):
42     _name = "stock.incoterms"
43     _description = "Incoterms"
44     _columns = {
45         'name': fields.char('Name', required=True, help="Incoterms are series of sales terms. They are used to divide transaction costs and responsibilities between buyer and seller and reflect state-of-the-art transportation practices."),
46         'code': fields.char('Code', size=3, required=True, help="Incoterm Standard Code"),
47         'active': fields.boolean('Active', help="By unchecking the active field, you may hide an INCOTERM you will not use."),
48     }
49     _defaults = {
50         'active': True,
51     }
52
53 #----------------------------------------------------------
54 # Stock Location
55 #----------------------------------------------------------
56
57 class stock_location(osv.osv):
58     _name = "stock.location"
59     _description = "Inventory Locations"
60     _parent_name = "location_id"
61     _parent_store = True
62     _parent_order = 'name'
63     _order = 'parent_left'
64     _rec_name = 'complete_name'
65
66     def _location_owner(self, cr, uid, location, context=None):
67         ''' Return the company owning the location if any '''
68         return location and (location.usage == 'internal') and location.company_id or False
69
70     def _complete_name(self, cr, uid, ids, name, args, context=None):
71         """ Forms complete name of location from parent location to child location.
72         @return: Dictionary of values
73         """
74         res = {}
75         for m in self.browse(cr, uid, ids, context=context):
76             res[m.id] = m.name
77             parent = m.location_id
78             while parent:
79                 res[m.id] = parent.name + ' / ' + res[m.id]
80                 parent = parent.location_id
81         return res
82
83     def _get_sublocations(self, cr, uid, ids, context=None):
84         """ return all sublocations of the given stock locations (included) """
85         if context is None:
86             context = {}
87         context_with_inactive = context.copy()
88         context_with_inactive['active_test'] = False
89         return self.search(cr, uid, [('id', 'child_of', ids)], context=context_with_inactive)
90
91     def _name_get(self, cr, uid, location, context=None):
92         name = location.name
93         while location.location_id and location.usage != 'view':
94             location = location.location_id
95             name = location.name + '/' + name
96         return name
97
98     def name_get(self, cr, uid, ids, context=None):
99         res = []
100         for location in self.browse(cr, uid, ids, context=context):
101             res.append((location.id, self._name_get(cr, uid, location, context=context)))
102         return res
103
104     _columns = {
105         'name': fields.char('Location Name', required=True, translate=True),
106         'active': fields.boolean('Active', help="By unchecking the active field, you may hide a location without deleting it."),
107         'usage': fields.selection([
108                         ('supplier', 'Supplier Location'),
109                         ('view', 'View'),
110                         ('internal', 'Internal Location'),
111                         ('customer', 'Customer Location'),
112                         ('inventory', 'Inventory'),
113                         ('procurement', 'Procurement'),
114                         ('production', 'Production'),
115                         ('transit', 'Transit Location')],
116                 'Location Type', required=True,
117                 help="""* Supplier Location: Virtual location representing the source location for products coming from your suppliers
118                        \n* View: Virtual location used to create a hierarchical structures for your warehouse, aggregating its child locations ; can't directly contain products
119                        \n* Internal Location: Physical locations inside your own warehouses,
120                        \n* Customer Location: Virtual location representing the destination location for products sent to your customers
121                        \n* Inventory: Virtual location serving as counterpart for inventory operations used to correct stock levels (Physical inventories)
122                        \n* Procurement: Virtual location serving as temporary counterpart for procurement operations when the source (supplier or production) is not known yet. This location should be empty when the procurement scheduler has finished running.
123                        \n* Production: Virtual counterpart location for production operations: this location consumes the raw material and produces finished products
124                        \n* Transit Location: Counterpart location that should be used in inter-companies or inter-warehouses operations
125                       """, select=True),
126         'complete_name': fields.function(_complete_name, type='char', string="Location Name",
127                             store={'stock.location': (_get_sublocations, ['name', 'location_id', 'active'], 10)}),
128         'location_id': fields.many2one('stock.location', 'Parent Location', select=True, ondelete='cascade'),
129         'child_ids': fields.one2many('stock.location', 'location_id', 'Contains'),
130
131         'partner_id': fields.many2one('res.partner', 'Owner', help="Owner of the location if not internal"),
132
133         'comment': fields.text('Additional Information'),
134         'posx': fields.integer('Corridor (X)', help="Optional localization details, for information purpose only"),
135         'posy': fields.integer('Shelves (Y)', help="Optional localization details, for information purpose only"),
136         'posz': fields.integer('Height (Z)', help="Optional localization details, for information purpose only"),
137
138         'parent_left': fields.integer('Left Parent', select=1),
139         'parent_right': fields.integer('Right Parent', select=1),
140
141         'company_id': fields.many2one('res.company', 'Company', select=1, help='Let this field empty if this location is shared between companies'),
142         'scrap_location': fields.boolean('Is a Scrap Location?', help='Check this box to allow using this location to put scrapped/damaged goods.'),
143         'removal_strategy_id': fields.many2one('product.removal', 'Removal Strategy', help="Defines the default method used for suggesting the exact location (shelf) where to take the products from, which lot etc. for this location. This method can be enforced at the product category level, and a fallback is made on the parent locations if none is set here."),
144         'putaway_strategy_id': fields.many2one('product.putaway', 'Put Away Strategy', help="Defines the default method used for suggesting the exact location (shelf) where to store the products. This method can be enforced at the product category level, and a fallback is made on the parent locations if none is set here."),
145         'loc_barcode': fields.char('Location Barcode'),
146     }
147     _defaults = {
148         'active': True,
149         'usage': 'internal',
150         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.location', context=c),
151         'posx': 0,
152         'posy': 0,
153         'posz': 0,
154         'scrap_location': False,
155     }
156     _sql_constraints = [('loc_barcode_company_uniq', 'unique (loc_barcode,company_id)', 'The barcode for a location must be unique per company !')]
157
158     def create(self, cr, uid, default, context=None):
159         if not default.get('loc_barcode', False):
160             default.update({'loc_barcode': default.get('complete_name', False)})
161         return super(stock_location, self).create(cr, uid, default, context=context)
162
163     def get_putaway_strategy(self, cr, uid, location, product, context=None):
164         ''' Returns the location where the product has to be put, if any compliant putaway strategy is found. Otherwise returns None.'''
165         putaway_obj = self.pool.get('product.putaway')
166         loc = location
167         while loc:
168             if loc.putaway_strategy_id:
169                 res = putaway_obj.putaway_apply(cr, uid, loc.putaway_strategy_id, product, context=context)
170                 if res:
171                     return res
172             loc = loc.location_id
173
174     def _default_removal_strategy(self, cr, uid, context=None):
175         return 'fifo'
176
177     def get_removal_strategy(self, cr, uid, location, product, context=None):
178         ''' Returns the removal strategy to consider for the given product and location.
179             :param location: browse record (stock.location)
180             :param product: browse record (product.product)
181             :rtype: char
182         '''
183         if product.categ_id.removal_strategy_id:
184             return product.categ_id.removal_strategy_id.method
185         loc = location
186         while loc:
187             if loc.removal_strategy_id:
188                 return loc.removal_strategy_id.method
189             loc = loc.location_id
190         return self._default_removal_strategy(cr, uid, context=context)
191
192
193     def get_warehouse(self, cr, uid, location, context=None):
194         """
195             Returns warehouse id of warehouse that contains location
196             :param location: browse record (stock.location)
197         """
198         wh_obj = self.pool.get("stock.warehouse")
199         whs = wh_obj.search(cr, uid, [('view_location_id.parent_left', '<=', location.parent_left), 
200                                 ('view_location_id.parent_right', '>=', location.parent_left)], context=context)
201         return whs and whs[0] or False
202
203 #----------------------------------------------------------
204 # Routes
205 #----------------------------------------------------------
206
207 class stock_location_route(osv.osv):
208     _name = 'stock.location.route'
209     _description = "Inventory Routes"
210     _order = 'sequence'
211
212     _columns = {
213         'name': fields.char('Route Name', required=True),
214         'sequence': fields.integer('Sequence'),
215         'pull_ids': fields.one2many('procurement.rule', 'route_id', 'Pull Rules', copy=True),
216         'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the route without removing it."),
217         'push_ids': fields.one2many('stock.location.path', 'route_id', 'Push Rules', copy=True),
218         'product_selectable': fields.boolean('Applicable on Product'),
219         'product_categ_selectable': fields.boolean('Applicable on Product Category'),
220         'warehouse_selectable': fields.boolean('Applicable on Warehouse'),
221         'supplied_wh_id': fields.many2one('stock.warehouse', 'Supplied Warehouse'),
222         'supplier_wh_id': fields.many2one('stock.warehouse', 'Supplier Warehouse'),
223         'company_id': fields.many2one('res.company', 'Company', select=1, help='Let this field empty if this route is shared between all companies'),
224     }
225
226     _defaults = {
227         'sequence': lambda self, cr, uid, ctx: 0,
228         'active': True,
229         'product_selectable': True,
230         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.location.route', context=c),
231     }
232
233     def write(self, cr, uid, ids, vals, context=None):
234         '''when a route is deactivated, deactivate also its pull and push rules'''
235         if isinstance(ids, (int, long)):
236             ids = [ids]
237         res = super(stock_location_route, self).write(cr, uid, ids, vals, context=context)
238         if 'active' in vals:
239             push_ids = []
240             pull_ids = []
241             for route in self.browse(cr, uid, ids, context=context):
242                 if route.push_ids:
243                     push_ids += [r.id for r in route.push_ids if r.active != vals['active']]
244                 if route.pull_ids:
245                     pull_ids += [r.id for r in route.pull_ids if r.active != vals['active']]
246             if push_ids:
247                 self.pool.get('stock.location.path').write(cr, uid, push_ids, {'active': vals['active']}, context=context)
248             if pull_ids:
249                 self.pool.get('procurement.rule').write(cr, uid, pull_ids, {'active': vals['active']}, context=context)
250         return res
251
252 #----------------------------------------------------------
253 # Quants
254 #----------------------------------------------------------
255
256 class stock_quant(osv.osv):
257     """
258     Quants are the smallest unit of stock physical instances
259     """
260     _name = "stock.quant"
261     _description = "Quants"
262
263     def _get_quant_name(self, cr, uid, ids, name, args, context=None):
264         """ Forms complete name of location from parent location to child location.
265         @return: Dictionary of values
266         """
267         res = {}
268         for q in self.browse(cr, uid, ids, context=context):
269
270             res[q.id] = q.product_id.code or ''
271             if q.lot_id:
272                 res[q.id] = q.lot_id.name
273             res[q.id] += ': ' + str(q.qty) + q.product_id.uom_id.name
274         return res
275
276     def _calc_inventory_value(self, cr, uid, ids, name, attr, context=None):
277         context = dict(context or {})
278         res = {}
279         uid_company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
280         for quant in self.browse(cr, uid, ids, context=context):
281             context.pop('force_company', None)
282             if quant.company_id.id != uid_company_id:
283                 #if the company of the quant is different than the current user company, force the company in the context
284                 #then re-do a browse to read the property fields for the good company.
285                 context['force_company'] = quant.company_id.id
286                 quant = self.browse(cr, uid, quant.id, context=context)
287             res[quant.id] = self._get_inventory_value(cr, uid, quant, context=context)
288         return res
289
290     def _get_inventory_value(self, cr, uid, quant, context=None):
291         return quant.product_id.standard_price * quant.qty
292
293     _columns = {
294         'name': fields.function(_get_quant_name, type='char', string='Identifier'),
295         'product_id': fields.many2one('product.product', 'Product', required=True, ondelete="restrict", readonly=True, select=True),
296         'location_id': fields.many2one('stock.location', 'Location', required=True, ondelete="restrict", readonly=True, select=True),
297         'qty': fields.float('Quantity', required=True, help="Quantity of products in this quant, in the default unit of measure of the product", readonly=True, select=True),
298         'package_id': fields.many2one('stock.quant.package', string='Package', help="The package containing this quant", readonly=True, select=True),
299         'packaging_type_id': fields.related('package_id', 'packaging_id', type='many2one', relation='product.packaging', string='Type of packaging', readonly=True, store=True),
300         'reservation_id': fields.many2one('stock.move', 'Reserved for Move', help="The move the quant is reserved for", readonly=True, select=True),
301         'lot_id': fields.many2one('stock.production.lot', 'Lot', readonly=True, select=True),
302         'cost': fields.float('Unit Cost'),
303         'owner_id': fields.many2one('res.partner', 'Owner', help="This is the owner of the quant", readonly=True, select=True),
304
305         'create_date': fields.datetime('Creation Date', readonly=True),
306         'in_date': fields.datetime('Incoming Date', readonly=True, select=True),
307
308         'history_ids': fields.many2many('stock.move', 'stock_quant_move_rel', 'quant_id', 'move_id', 'Moves', help='Moves that operate(d) on this quant'),
309         'company_id': fields.many2one('res.company', 'Company', help="The company to which the quants belong", required=True, readonly=True, select=True),
310         'inventory_value': fields.function(_calc_inventory_value, string="Inventory Value", type='float', readonly=True),
311
312         # Used for negative quants to reconcile after compensated by a new positive one
313         'propagated_from_id': fields.many2one('stock.quant', 'Linked Quant', help='The negative quant this is coming from', readonly=True, select=True),
314         'negative_move_id': fields.many2one('stock.move', 'Move Negative Quant', help='If this is a negative quant, this will be the move that caused this negative quant.', readonly=True),
315         'negative_dest_location_id': fields.related('negative_move_id', 'location_dest_id', type='many2one', relation='stock.location', string="Negative Destination Location", readonly=True, 
316                                                     help="Technical field used to record the destination location of a move that created a negative quant"),
317     }
318
319     _defaults = {
320         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.quant', context=c),
321     }
322
323     def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False, lazy=True):
324         ''' Overwrite the read_group in order to sum the function field 'inventory_value' in group by'''
325         res = super(stock_quant, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby, lazy=lazy)
326         if 'inventory_value' in fields:
327             for line in res:
328                 if '__domain' in line:
329                     lines = self.search(cr, uid, line['__domain'], context=context)
330                     inv_value = 0.0
331                     for line2 in self.browse(cr, uid, lines, context=context):
332                         inv_value += line2.inventory_value
333                     line['inventory_value'] = inv_value
334         return res
335
336     def action_view_quant_history(self, cr, uid, ids, context=None):
337         '''
338         This function returns an action that display the history of the quant, which
339         mean all the stock moves that lead to this quant creation with this quant quantity.
340         '''
341         mod_obj = self.pool.get('ir.model.data')
342         act_obj = self.pool.get('ir.actions.act_window')
343
344         result = mod_obj.get_object_reference(cr, uid, 'stock', 'action_move_form2')
345         id = result and result[1] or False
346         result = act_obj.read(cr, uid, [id], context={})[0]
347
348         move_ids = []
349         for quant in self.browse(cr, uid, ids, context=context):
350             move_ids += [move.id for move in quant.history_ids]
351
352         result['domain'] = "[('id','in',[" + ','.join(map(str, move_ids)) + "])]"
353         return result
354
355     def quants_reserve(self, cr, uid, quants, move, link=False, context=None):
356         '''This function reserves quants for the given move (and optionally given link). If the total of quantity reserved is enough, the move's state
357         is also set to 'assigned'
358
359         :param quants: list of tuple(quant browse record or None, qty to reserve). If None is given as first tuple element, the item will be ignored. Negative quants should not be received as argument
360         :param move: browse record
361         :param link: browse record (stock.move.operation.link)
362         '''
363         toreserve = []
364         reserved_availability = move.reserved_availability
365         #split quants if needed
366         for quant, qty in quants:
367             if qty <= 0.0 or (quant and quant.qty <= 0.0):
368                 raise osv.except_osv(_('Error!'), _('You can not reserve a negative quantity or a negative quant.'))
369             if not quant:
370                 continue
371             self._quant_split(cr, uid, quant, qty, context=context)
372             toreserve.append(quant.id)
373             reserved_availability += quant.qty
374         #reserve quants
375         if toreserve:
376             self.write(cr, SUPERUSER_ID, toreserve, {'reservation_id': move.id}, context=context)
377             #if move has a picking_id, write on that picking that pack_operation might have changed and need to be recomputed
378             if move.picking_id:
379                 self.pool.get('stock.picking').write(cr, uid, [move.picking_id.id], {'recompute_pack_op': True}, context=context)
380         #check if move'state needs to be set as 'assigned'
381         rounding = move.product_id.uom_id.rounding
382         if float_compare(reserved_availability, move.product_qty, precision_rounding=rounding) == 0 and move.state in ('confirmed', 'waiting')  :
383             self.pool.get('stock.move').write(cr, uid, [move.id], {'state': 'assigned'}, context=context)
384         elif float_compare(reserved_availability, 0, precision_rounding=rounding) > 0 and not move.partially_available:
385             self.pool.get('stock.move').write(cr, uid, [move.id], {'partially_available': True}, context=context)
386
387     def quants_move(self, cr, uid, quants, move, location_to, location_from=False, lot_id=False, owner_id=False, src_package_id=False, dest_package_id=False, context=None):
388         """Moves all given stock.quant in the given destination location.  Unreserve from current move.
389         :param quants: list of tuple(browse record(stock.quant) or None, quantity to move)
390         :param move: browse record (stock.move)
391         :param location_to: browse record (stock.location) depicting where the quants have to be moved
392         :param location_from: optional browse record (stock.location) explaining where the quant has to be taken (may differ from the move source location in case a removal strategy applied). This parameter is only used to pass to _quant_create if a negative quant must be created
393         :param lot_id: ID of the lot that must be set on the quants to move
394         :param owner_id: ID of the partner that must own the quants to move
395         :param src_package_id: ID of the package that contains the quants to move
396         :param dest_package_id: ID of the package that must be set on the moved quant
397         """
398         quants_reconcile = []
399         to_move_quants = []
400         self._check_location(cr, uid, location_to, context=context)
401         for quant, qty in quants:
402             if not quant:
403                 #If quant is None, we will create a quant to move (and potentially a negative counterpart too)
404                 quant = self._quant_create(cr, uid, qty, move, lot_id=lot_id, owner_id=owner_id, src_package_id=src_package_id, dest_package_id=dest_package_id, force_location_from=location_from, force_location_to=location_to, context=context)
405             else:
406                 self._quant_split(cr, uid, quant, qty, context=context)
407                 quant.refresh()
408                 to_move_quants.append(quant)
409             quants_reconcile.append(quant)
410         if to_move_quants:
411             to_recompute_move_ids = [x.reservation_id.id for x in to_move_quants if x.reservation_id and x.reservation_id.id != move.id]
412             self.move_quants_write(cr, uid, to_move_quants, move, location_to, dest_package_id, context=context)
413             self.pool.get('stock.move').recalculate_move_state(cr, uid, to_recompute_move_ids, context=context)
414         if location_to.usage == 'internal':
415             if self.search(cr, uid, [('product_id', '=', move.product_id.id), ('qty','<', 0)], limit=1, context=context):
416                 for quant in quants_reconcile:
417                     quant.refresh()
418                     self._quant_reconcile_negative(cr, uid, quant, move, context=context)
419
420     def move_quants_write(self, cr, uid, quants, move, location_dest_id, dest_package_id, context=None):
421         vals = {'location_id': location_dest_id.id,
422                 'history_ids': [(4, move.id)],
423                 'package_id': dest_package_id,
424                 'reservation_id': False}
425         self.write(cr, SUPERUSER_ID, [q.id for q in quants], vals, context=context)
426
427     def quants_get_prefered_domain(self, cr, uid, location, product, qty, domain=None, prefered_domain_list=[], restrict_lot_id=False, restrict_partner_id=False, context=None):
428         ''' This function tries to find quants in the given location for the given domain, by trying to first limit
429             the choice on the quants that match the first item of prefered_domain_list as well. But if the qty requested is not reached
430             it tries to find the remaining quantity by looping on the prefered_domain_list (tries with the second item and so on).
431             Make sure the quants aren't found twice => all the domains of prefered_domain_list should be orthogonal
432         '''
433         if domain is None:
434             domain = []
435         quants = [(None, qty)]
436         #don't look for quants in location that are of type production, supplier or inventory.
437         if location.usage in ['inventory', 'production', 'supplier']:
438             return quants
439         res_qty = qty
440         if not prefered_domain_list:
441             return self.quants_get(cr, uid, location, product, qty, domain=domain, restrict_lot_id=restrict_lot_id, restrict_partner_id=restrict_partner_id, context=context)
442         for prefered_domain in prefered_domain_list:
443             res_qty_cmp = float_compare(res_qty, 0, precision_rounding=product.uom_id.rounding)
444             if res_qty_cmp > 0:
445                 #try to replace the last tuple (None, res_qty) with something that wasn't chosen at first because of the prefered order
446                 quants.pop()
447                 tmp_quants = self.quants_get(cr, uid, location, product, res_qty, domain=domain + prefered_domain, restrict_lot_id=restrict_lot_id, restrict_partner_id=restrict_partner_id, context=context)
448                 for quant in tmp_quants:
449                     if quant[0]:
450                         res_qty -= quant[1]
451                 quants += tmp_quants
452         return quants
453
454     def quants_get(self, cr, uid, location, product, qty, domain=None, restrict_lot_id=False, restrict_partner_id=False, context=None):
455         """
456         Use the removal strategies of product to search for the correct quants
457         If you inherit, put the super at the end of your method.
458
459         :location: browse record of the parent location where the quants have to be found
460         :product: browse record of the product to find
461         :qty in UoM of product
462         """
463         result = []
464         domain = domain or [('qty', '>', 0.0)]
465         if restrict_partner_id:
466             domain += [('owner_id', '=', restrict_partner_id)]
467         if restrict_lot_id:
468             domain += [('lot_id', '=', restrict_lot_id)]
469         if location:
470             removal_strategy = self.pool.get('stock.location').get_removal_strategy(cr, uid, location, product, context=context)
471             result += self.apply_removal_strategy(cr, uid, location, product, qty, domain, removal_strategy, context=context)
472         return result
473
474     def apply_removal_strategy(self, cr, uid, location, product, quantity, domain, removal_strategy, context=None):
475         if removal_strategy == 'fifo':
476             order = 'in_date, id'
477             return self._quants_get_order(cr, uid, location, product, quantity, domain, order, context=context)
478         elif removal_strategy == 'lifo':
479             order = 'in_date desc, id desc'
480             return self._quants_get_order(cr, uid, location, product, quantity, domain, order, context=context)
481         raise osv.except_osv(_('Error!'), _('Removal strategy %s not implemented.' % (removal_strategy,)))
482
483     def _quant_create(self, cr, uid, qty, move, lot_id=False, owner_id=False, src_package_id=False, dest_package_id=False,
484                       force_location_from=False, force_location_to=False, context=None):
485         '''Create a quant in the destination location and create a negative quant in the source location if it's an internal location.
486         '''
487         if context is None:
488             context = {}
489         price_unit = self.pool.get('stock.move').get_price_unit(cr, uid, move, context=context)
490         location = force_location_to or move.location_dest_id
491         rounding = move.product_id.uom_id.rounding
492         vals = {
493             'product_id': move.product_id.id,
494             'location_id': location.id,
495             'qty': float_round(qty, precision_rounding=rounding),
496             'cost': price_unit,
497             'history_ids': [(4, move.id)],
498             'in_date': datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
499             'company_id': move.company_id.id,
500             'lot_id': lot_id,
501             'owner_id': owner_id,
502             'package_id': dest_package_id,
503         }
504
505         if move.location_id.usage == 'internal':
506             #if we were trying to move something from an internal location and reach here (quant creation),
507             #it means that a negative quant has to be created as well.
508             negative_vals = vals.copy()
509             negative_vals['location_id'] = force_location_from and force_location_from.id or move.location_id.id
510             negative_vals['qty'] = float_round(-qty, precision_rounding=rounding)
511             negative_vals['cost'] = price_unit
512             negative_vals['negative_move_id'] = move.id
513             negative_vals['package_id'] = src_package_id
514             negative_quant_id = self.create(cr, SUPERUSER_ID, negative_vals, context=context)
515             vals.update({'propagated_from_id': negative_quant_id})
516
517         #create the quant as superuser, because we want to restrict the creation of quant manually: we should always use this method to create quants
518         quant_id = self.create(cr, SUPERUSER_ID, vals, context=context)
519         return self.browse(cr, uid, quant_id, context=context)
520
521     def _quant_split(self, cr, uid, quant, qty, context=None):
522         context = context or {}
523         rounding = quant.product_id.uom_id.rounding
524         if float_compare(abs(quant.qty), abs(qty), precision_rounding=rounding) <= 0: # if quant <= qty in abs, take it entirely
525             return False
526         qty_round = float_round(qty, precision_rounding=rounding)
527         new_qty_round = float_round(quant.qty - qty, precision_rounding=rounding)
528         new_quant = self.copy(cr, SUPERUSER_ID, quant.id, default={'qty': new_qty_round, 'history_ids': [(4, x.id) for x in quant.history_ids]}, context=context)
529         self.write(cr, SUPERUSER_ID, quant.id, {'qty': qty_round}, context=context)
530         quant.refresh()
531         return self.browse(cr, uid, new_quant, context=context)
532
533     def _get_latest_move(self, cr, uid, quant, context=None):
534         move = False
535         for m in quant.history_ids:
536             if not move or m.date > move.date:
537                 move = m
538         return move
539
540     @api.cr_uid_ids_context
541     def _quants_merge(self, cr, uid, solved_quant_ids, solving_quant, context=None):
542         path = []
543         for move in solving_quant.history_ids:
544             path.append((4, move.id))
545         self.write(cr, SUPERUSER_ID, solved_quant_ids, {'history_ids': path}, context=context)
546
547     def _quant_reconcile_negative(self, cr, uid, quant, move, context=None):
548         """
549             When new quant arrive in a location, try to reconcile it with
550             negative quants. If it's possible, apply the cost of the new
551             quant to the conter-part of the negative quant.
552         """
553         solving_quant = quant
554         dom = [('qty', '<', 0)]
555         if quant.lot_id:
556             dom += [('lot_id', '=', quant.lot_id.id)]
557         dom += [('owner_id', '=', quant.owner_id.id)]
558         dom += [('package_id', '=', quant.package_id.id)]
559         dom += [('id', '!=', quant.propagated_from_id.id)]
560         quants = self.quants_get(cr, uid, quant.location_id, quant.product_id, quant.qty, dom, context=context)
561         product_uom_rounding = quant.product_id.uom_id.rounding
562         for quant_neg, qty in quants:
563             if not quant_neg or not solving_quant:
564                 continue
565             to_solve_quant_ids = self.search(cr, uid, [('propagated_from_id', '=', quant_neg.id)], context=context)
566             if not to_solve_quant_ids:
567                 continue
568             solving_qty = qty
569             solved_quant_ids = []
570             for to_solve_quant in self.browse(cr, uid, to_solve_quant_ids, context=context):
571                 if float_compare(solving_qty, 0, precision_rounding=product_uom_rounding) <= 0:
572                     continue
573                 solved_quant_ids.append(to_solve_quant.id)
574                 self._quant_split(cr, uid, to_solve_quant, min(solving_qty, to_solve_quant.qty), context=context)
575                 solving_qty -= min(solving_qty, to_solve_quant.qty)
576             remaining_solving_quant = self._quant_split(cr, uid, solving_quant, qty, context=context)
577             remaining_neg_quant = self._quant_split(cr, uid, quant_neg, -qty, context=context)
578             #if the reconciliation was not complete, we need to link together the remaining parts
579             if remaining_neg_quant:
580                 remaining_to_solve_quant_ids = self.search(cr, uid, [('propagated_from_id', '=', quant_neg.id), ('id', 'not in', solved_quant_ids)], context=context)
581                 if remaining_to_solve_quant_ids:
582                     self.write(cr, SUPERUSER_ID, remaining_to_solve_quant_ids, {'propagated_from_id': remaining_neg_quant.id}, context=context)
583             if solving_quant.propagated_from_id and solved_quant_ids:
584                 self.write(cr, uid, solved_quant_ids, {'propagated_from_id': solving_quant.propagated_from_id.id}, context=context)
585             #delete the reconciled quants, as it is replaced by the solved quants
586             self.unlink(cr, SUPERUSER_ID, [quant_neg.id], context=context)
587             if solved_quant_ids:
588                 #price update + accounting entries adjustments
589                 self._price_update(cr, uid, solved_quant_ids, solving_quant.cost, context=context)
590                 #merge history (and cost?)
591                 self._quants_merge(cr, uid, solved_quant_ids, solving_quant, context=context)
592             self.unlink(cr, SUPERUSER_ID, [solving_quant.id], context=context)
593             solving_quant = remaining_solving_quant
594
595     def _price_update(self, cr, uid, ids, newprice, context=None):
596         self.write(cr, SUPERUSER_ID, ids, {'cost': newprice}, context=context)
597
598     def quants_unreserve(self, cr, uid, move, context=None):
599         related_quants = [x.id for x in move.reserved_quant_ids]
600         if related_quants:
601             #if move has a picking_id, write on that picking that pack_operation might have changed and need to be recomputed
602             if move.picking_id:
603                 self.pool.get('stock.picking').write(cr, uid, [move.picking_id.id], {'recompute_pack_op': True}, context=context)
604             if move.partially_available:
605                 self.pool.get("stock.move").write(cr, uid, [move.id], {'partially_available': False}, context=context)
606             self.write(cr, SUPERUSER_ID, related_quants, {'reservation_id': False}, context=context)
607
608     def _quants_get_order(self, cr, uid, location, product, quantity, domain=[], orderby='in_date', context=None):
609         ''' Implementation of removal strategies
610             If it can not reserve, it will return a tuple (None, qty)
611         '''
612         if context is None:
613             context = {}
614         domain += location and [('location_id', 'child_of', location.id)] or []
615         domain += [('product_id', '=', product.id)]
616         if context.get('force_company'):
617             domain += [('company_id', '=', context.get('force_company'))]
618         else:
619             domain += [('company_id', '=', self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id)]
620         res = []
621         offset = 0
622         while float_compare(quantity, 0, precision_rounding=product.uom_id.rounding) > 0:
623             quants = self.search(cr, uid, domain, order=orderby, limit=10, offset=offset, context=context)
624             if not quants:
625                 res.append((None, quantity))
626                 break
627             for quant in self.browse(cr, uid, quants, context=context):
628                 rounding = product.uom_id.rounding
629                 if float_compare(quantity, abs(quant.qty), precision_rounding=rounding) >= 0:
630                     res += [(quant, abs(quant.qty))]
631                     quantity -= abs(quant.qty)
632                 elif float_compare(quantity, 0.0, precision_rounding=rounding) != 0:
633                     res += [(quant, quantity)]
634                     quantity = 0
635                     break
636             offset += 10
637         return res
638
639     def _check_location(self, cr, uid, location, context=None):
640         if location.usage == 'view':
641             raise osv.except_osv(_('Error'), _('You cannot move to a location of type view %s.') % (location.name))
642         return True
643
644 #----------------------------------------------------------
645 # Stock Picking
646 #----------------------------------------------------------
647
648 class stock_picking(osv.osv):
649     _name = "stock.picking"
650     _inherit = ['mail.thread']
651     _description = "Picking List"
652     _order = "priority desc, date asc, id desc"
653
654     def _set_min_date(self, cr, uid, id, field, value, arg, context=None):
655         move_obj = self.pool.get("stock.move")
656         if value:
657             move_ids = [move.id for move in self.browse(cr, uid, id, context=context).move_lines]
658             move_obj.write(cr, uid, move_ids, {'date_expected': value}, context=context)
659
660     def _set_priority(self, cr, uid, id, field, value, arg, context=None):
661         move_obj = self.pool.get("stock.move")
662         if value:
663             move_ids = [move.id for move in self.browse(cr, uid, id, context=context).move_lines]
664             move_obj.write(cr, uid, move_ids, {'priority': value}, context=context)
665
666     def get_min_max_date(self, cr, uid, ids, field_name, arg, context=None):
667         """ Finds minimum and maximum dates for picking.
668         @return: Dictionary of values
669         """
670         res = {}
671         for id in ids:
672             res[id] = {'min_date': False, 'max_date': False, 'priority': '1'}
673         if not ids:
674             return res
675         cr.execute("""select
676                 picking_id,
677                 min(date_expected),
678                 max(date_expected),
679                 max(priority)
680             from
681                 stock_move
682             where
683                 picking_id IN %s
684             group by
685                 picking_id""", (tuple(ids),))
686         for pick, dt1, dt2, prio in cr.fetchall():
687             res[pick]['min_date'] = dt1
688             res[pick]['max_date'] = dt2
689             res[pick]['priority'] = prio
690         return res
691
692     def create(self, cr, user, vals, context=None):
693         context = context or {}
694         if ('name' not in vals) or (vals.get('name') in ('/', False)):
695             ptype_id = vals.get('picking_type_id', context.get('default_picking_type_id', False))
696             sequence_id = self.pool.get('stock.picking.type').browse(cr, user, ptype_id, context=context).sequence_id.id
697             vals['name'] = self.pool.get('ir.sequence').get_id(cr, user, sequence_id, 'id', context=context)
698         return super(stock_picking, self).create(cr, user, vals, context)
699
700     def _state_get(self, cr, uid, ids, field_name, arg, context=None):
701         '''The state of a picking depends on the state of its related stock.move
702             draft: the picking has no line or any one of the lines is draft
703             done, draft, cancel: all lines are done / draft / cancel
704             confirmed, waiting, assigned, partially_available depends on move_type (all at once or partial)
705         '''
706         res = {}
707         for pick in self.browse(cr, uid, ids, context=context):
708             if (not pick.move_lines) or any([x.state == 'draft' for x in pick.move_lines]):
709                 res[pick.id] = 'draft'
710                 continue
711             if all([x.state == 'cancel' for x in pick.move_lines]):
712                 res[pick.id] = 'cancel'
713                 continue
714             if all([x.state in ('cancel', 'done') for x in pick.move_lines]):
715                 res[pick.id] = 'done'
716                 continue
717
718             order = {'confirmed': 0, 'waiting': 1, 'assigned': 2}
719             order_inv = {0: 'confirmed', 1: 'waiting', 2: 'assigned'}
720             lst = [order[x.state] for x in pick.move_lines if x.state not in ('cancel', 'done')]
721             if pick.move_type == 'one':
722                 res[pick.id] = order_inv[min(lst)]
723             else:
724                 #we are in the case of partial delivery, so if all move are assigned, picking
725                 #should be assign too, else if one of the move is assigned, or partially available, picking should be
726                 #in partially available state, otherwise, picking is in waiting or confirmed state
727                 res[pick.id] = order_inv[max(lst)]
728                 if not all(x == 2 for x in lst):
729                     if any(x == 2 for x in lst):
730                         res[pick.id] = 'partially_available'
731                     else:
732                         #if all moves aren't assigned, check if we have one product partially available
733                         for move in pick.move_lines:
734                             if move.partially_available:
735                                 res[pick.id] = 'partially_available'
736                                 break
737         return res
738
739     def _get_pickings(self, cr, uid, ids, context=None):
740         res = set()
741         for move in self.browse(cr, uid, ids, context=context):
742             if move.picking_id:
743                 res.add(move.picking_id.id)
744         return list(res)
745
746     def _get_pack_operation_exist(self, cr, uid, ids, field_name, arg, context=None):
747         res = {}
748         for pick in self.browse(cr, uid, ids, context=context):
749             res[pick.id] = False
750             if pick.pack_operation_ids:
751                 res[pick.id] = True
752         return res
753
754     def _get_quant_reserved_exist(self, cr, uid, ids, field_name, arg, context=None):
755         res = {}
756         for pick in self.browse(cr, uid, ids, context=context):
757             res[pick.id] = False
758             for move in pick.move_lines:
759                 if move.reserved_quant_ids:
760                     res[pick.id] = True
761                     continue
762         return res
763
764     def check_group_lot(self, cr, uid, context=None):
765         """ This function will return true if we have the setting to use lots activated. """
766         return self.pool.get('res.users').has_group(cr, uid, 'stock.group_production_lot')
767
768     def check_group_pack(self, cr, uid, context=None):
769         """ This function will return true if we have the setting to use package activated. """
770         return self.pool.get('res.users').has_group(cr, uid, 'stock.group_tracking_lot')
771
772     def action_assign_owner(self, cr, uid, ids, context=None):
773         for picking in self.browse(cr, uid, ids, context=context):
774             packop_ids = [op.id for op in picking.pack_operation_ids]
775             self.pool.get('stock.pack.operation').write(cr, uid, packop_ids, {'owner_id': picking.owner_id.id}, context=context)
776
777     _columns = {
778         'name': fields.char('Reference', select=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, copy=False),
779         'origin': fields.char('Source Document', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, help="Reference of the document", select=True),
780         'backorder_id': fields.many2one('stock.picking', 'Back Order of', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, help="If this shipment was split, then this field links to the shipment which contains the already processed part.", select=True, copy=False),
781         'note': fields.text('Notes', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}),
782         'move_type': fields.selection([('direct', 'Partial'), ('one', 'All at once')], 'Delivery Method', required=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, help="It specifies goods to be deliver partially or all at once"),
783         'state': fields.function(_state_get, type="selection", copy=False,
784             store={
785                 'stock.picking': (lambda self, cr, uid, ids, ctx: ids, ['move_type'], 20),
786                 'stock.move': (_get_pickings, ['state', 'picking_id', 'partially_available'], 20)},
787             selection=[
788                 ('draft', 'Draft'),
789                 ('cancel', 'Cancelled'),
790                 ('waiting', 'Waiting Another Operation'),
791                 ('confirmed', 'Waiting Availability'),
792                 ('partially_available', 'Partially Available'),
793                 ('assigned', 'Ready to Transfer'),
794                 ('done', 'Transferred'),
795                 ], string='Status', readonly=True, select=True, track_visibility='onchange',
796             help="""
797                 * Draft: not confirmed yet and will not be scheduled until confirmed\n
798                 * Waiting Another Operation: waiting for another move to proceed before it becomes automatically available (e.g. in Make-To-Order flows)\n
799                 * Waiting Availability: still waiting for the availability of products\n
800                 * Partially Available: some products are available and reserved\n
801                 * Ready to Transfer: products reserved, simply waiting for confirmation.\n
802                 * Transferred: has been processed, can't be modified or cancelled anymore\n
803                 * Cancelled: has been cancelled, can't be confirmed anymore"""
804         ),
805         'priority': fields.function(get_min_max_date, multi="min_max_date", fnct_inv=_set_priority, type='selection', selection=procurement.PROCUREMENT_PRIORITIES, string='Priority',
806                                     store={'stock.move': (_get_pickings, ['priority', 'picking_id'], 20)}, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, select=1, help="Priority for this picking. Setting manually a value here would set it as priority for all the moves",
807                                     track_visibility='onchange', required=True),
808         'min_date': fields.function(get_min_max_date, multi="min_max_date", fnct_inv=_set_min_date,
809                  store={'stock.move': (_get_pickings, ['date_expected', 'picking_id'], 20)}, type='datetime', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, string='Scheduled Date', select=1, help="Scheduled time for the first part of the shipment to be processed. Setting manually a value here would set it as expected date for all the stock moves.", track_visibility='onchange'),
810         'max_date': fields.function(get_min_max_date, multi="min_max_date",
811                  store={'stock.move': (_get_pickings, ['date_expected', 'picking_id'], 20)}, type='datetime', string='Max. Expected Date', select=2, help="Scheduled time for the last part of the shipment to be processed"),
812         'date': fields.datetime('Creation Date', help="Creation Date, usually the time of the order", select=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, track_visibility='onchange'),
813         'date_done': fields.datetime('Date of Transfer', help="Date of Completion", states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, copy=False),
814         'move_lines': fields.one2many('stock.move', 'picking_id', 'Internal Moves', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, copy=True),
815         'quant_reserved_exist': fields.function(_get_quant_reserved_exist, type='boolean', string='Quant already reserved ?', help='technical field used to know if there is already at least one quant reserved on moves of a given picking'),
816         'partner_id': fields.many2one('res.partner', 'Partner', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}),
817         'company_id': fields.many2one('res.company', 'Company', required=True, select=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}),
818         'pack_operation_ids': fields.one2many('stock.pack.operation', 'picking_id', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, string='Related Packing Operations'),
819         'pack_operation_exist': fields.function(_get_pack_operation_exist, type='boolean', string='Pack Operation Exists?', help='technical field for attrs in view'),
820         'picking_type_id': fields.many2one('stock.picking.type', 'Picking Type', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, required=True),
821         'picking_type_code': fields.related('picking_type_id', 'code', type='char', string='Picking Type Code', help="Technical field used to display the correct label on print button in the picking view"),
822
823         'owner_id': fields.many2one('res.partner', 'Owner', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, help="Default Owner"),
824         # Used to search on pickings
825         'product_id': fields.related('move_lines', 'product_id', type='many2one', relation='product.product', string='Product'),
826         'recompute_pack_op': fields.boolean('Recompute pack operation?', help='True if reserved quants changed, which mean we might need to recompute the package operations', copy=False),
827         'location_id': fields.related('move_lines', 'location_id', type='many2one', relation='stock.location', string='Location', readonly=True),
828         'location_dest_id': fields.related('move_lines', 'location_dest_id', type='many2one', relation='stock.location', string='Destination Location', readonly=True),
829         'group_id': fields.related('move_lines', 'group_id', type='many2one', relation='procurement.group', string='Procurement Group', readonly=True,
830               store={
831                   'stock.picking': (lambda self, cr, uid, ids, ctx: ids, ['move_lines'], 10),
832                   'stock.move': (_get_pickings, ['group_id', 'picking_id'], 10),
833               }),
834     }
835
836     _defaults = {
837         'name': '/',
838         'state': 'draft',
839         'move_type': 'direct',
840         'priority': '1',  # normal
841         'date': fields.datetime.now,
842         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.picking', context=c),
843         'recompute_pack_op': True,
844     }
845     _sql_constraints = [
846         ('name_uniq', 'unique(name, company_id)', 'Reference must be unique per company!'),
847     ]
848
849     def do_print_picking(self, cr, uid, ids, context=None):
850         '''This function prints the picking list'''
851         context = dict(context or {}, active_ids=ids)
852         return self.pool.get("report").get_action(cr, uid, ids, 'stock.report_picking', context=context)
853
854
855     def action_confirm(self, cr, uid, ids, context=None):
856         todo = []
857         todo_force_assign = []
858         for picking in self.browse(cr, uid, ids, context=context):
859             if picking.location_id.usage in ('supplier', 'inventory', 'production'):
860                 todo_force_assign.append(picking.id)
861             for r in picking.move_lines:
862                 if r.state == 'draft':
863                     todo.append(r.id)
864         if len(todo):
865             self.pool.get('stock.move').action_confirm(cr, uid, todo, context=context)
866
867         if todo_force_assign:
868             self.force_assign(cr, uid, todo_force_assign, context=context)
869         return True
870
871     def action_assign(self, cr, uid, ids, context=None):
872         """ Check availability of picking moves.
873         This has the effect of changing the state and reserve quants on available moves, and may
874         also impact the state of the picking as it is computed based on move's states.
875         @return: True
876         """
877         for pick in self.browse(cr, uid, ids, context=context):
878             if pick.state == 'draft':
879                 self.action_confirm(cr, uid, [pick.id], context=context)
880             pick.refresh()
881             #skip the moves that don't need to be checked
882             move_ids = [x.id for x in pick.move_lines if x.state not in ('draft', 'cancel', 'done')]
883             if not move_ids:
884                 raise osv.except_osv(_('Warning!'), _('Nothing to check the availability for.'))
885             self.pool.get('stock.move').action_assign(cr, uid, move_ids, context=context)
886         return True
887
888     def force_assign(self, cr, uid, ids, context=None):
889         """ Changes state of picking to available if moves are confirmed or waiting.
890         @return: True
891         """
892         for pick in self.browse(cr, uid, ids, context=context):
893             move_ids = [x.id for x in pick.move_lines if x.state in ['confirmed', 'waiting']]
894             self.pool.get('stock.move').force_assign(cr, uid, move_ids, context=context)
895         #pack_operation might have changed and need to be recomputed
896         self.write(cr, uid, ids, {'recompute_pack_op': True}, context=context)
897         return True
898
899     def action_cancel(self, cr, uid, ids, context=None):
900         for pick in self.browse(cr, uid, ids, context=context):
901             ids2 = [move.id for move in pick.move_lines]
902             self.pool.get('stock.move').action_cancel(cr, uid, ids2, context)
903         return True
904
905     def action_done(self, cr, uid, ids, context=None):
906         """Changes picking state to done by processing the Stock Moves of the Picking
907
908         Normally that happens when the button "Done" is pressed on a Picking view.
909         @return: True
910         """
911         for pick in self.browse(cr, uid, ids, context=context):
912             todo = []
913             for move in pick.move_lines:
914                 if move.state == 'draft':
915                     todo.extend(self.pool.get('stock.move').action_confirm(cr, uid, [move.id], context=context))
916                 elif move.state in ('assigned', 'confirmed'):
917                     todo.append(move.id)
918             if len(todo):
919                 self.pool.get('stock.move').action_done(cr, uid, todo, context=context)
920         return True
921
922     def unlink(self, cr, uid, ids, context=None):
923         #on picking deletion, cancel its move then unlink them too
924         move_obj = self.pool.get('stock.move')
925         context = context or {}
926         for pick in self.browse(cr, uid, ids, context=context):
927             move_ids = [move.id for move in pick.move_lines]
928             move_obj.action_cancel(cr, uid, move_ids, context=context)
929             move_obj.unlink(cr, uid, move_ids, context=context)
930         return super(stock_picking, self).unlink(cr, uid, ids, context=context)
931
932     def write(self, cr, uid, ids, vals, context=None):
933         res = super(stock_picking, self).write(cr, uid, ids, vals, context=context)
934         #if we changed the move lines or the pack operations, we need to recompute the remaining quantities of both
935         if 'move_lines' in vals or 'pack_operation_ids' in vals:
936             self.do_recompute_remaining_quantities(cr, uid, ids, context=context)
937         return res
938
939     def _create_backorder(self, cr, uid, picking, backorder_moves=[], context=None):
940         """ Move all non-done lines into a new backorder picking. If the key 'do_only_split' is given in the context, then move all lines not in context.get('split', []) instead of all non-done lines.
941         """
942         if not backorder_moves:
943             backorder_moves = picking.move_lines
944         backorder_move_ids = [x.id for x in backorder_moves if x.state not in ('done', 'cancel')]
945         if 'do_only_split' in context and context['do_only_split']:
946             backorder_move_ids = [x.id for x in backorder_moves if x.id not in context.get('split', [])]
947
948         if backorder_move_ids:
949             backorder_id = self.copy(cr, uid, picking.id, {
950                 'name': '/',
951                 'move_lines': [],
952                 'pack_operation_ids': [],
953                 'backorder_id': picking.id,
954             })
955             backorder = self.browse(cr, uid, backorder_id, context=context)
956             self.message_post(cr, uid, picking.id, body=_("Back order <em>%s</em> <b>created</b>.") % (backorder.name), context=context)
957             move_obj = self.pool.get("stock.move")
958             move_obj.write(cr, uid, backorder_move_ids, {'picking_id': backorder_id}, context=context)
959
960             self.write(cr, uid, [picking.id], {'date_done': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context)
961             self.action_confirm(cr, uid, [backorder_id], context=context)
962             return backorder_id
963         return False
964
965     @api.cr_uid_ids_context
966     def recheck_availability(self, cr, uid, picking_ids, context=None):
967         self.action_assign(cr, uid, picking_ids, context=context)
968         self.do_prepare_partial(cr, uid, picking_ids, context=context)
969
970     def _get_top_level_packages(self, cr, uid, quants_suggested_locations, context=None):
971         """This method searches for the higher level packages that can be moved as a single operation, given a list of quants
972            to move and their suggested destination, and returns the list of matching packages.
973         """
974         # Try to find as much as possible top-level packages that can be moved
975         pack_obj = self.pool.get("stock.quant.package")
976         quant_obj = self.pool.get("stock.quant")
977         top_lvl_packages = set()
978         quants_to_compare = quants_suggested_locations.keys()
979         for pack in list(set([x.package_id for x in quants_suggested_locations.keys() if x and x.package_id])):
980             loop = True
981             test_pack = pack
982             good_pack = False
983             pack_destination = False
984             while loop:
985                 pack_quants = pack_obj.get_content(cr, uid, [test_pack.id], context=context)
986                 all_in = True
987                 for quant in quant_obj.browse(cr, uid, pack_quants, context=context):
988                     # If the quant is not in the quants to compare and not in the common location
989                     if not quant in quants_to_compare:
990                         all_in = False
991                         break
992                     else:
993                         #if putaway strat apply, the destination location of each quant may be different (and thus the package should not be taken as a single operation)
994                         if not pack_destination:
995                             pack_destination = quants_suggested_locations[quant]
996                         elif pack_destination != quants_suggested_locations[quant]:
997                             all_in = False
998                             break
999                 if all_in:
1000                     good_pack = test_pack
1001                     if test_pack.parent_id:
1002                         test_pack = test_pack.parent_id
1003                     else:
1004                         #stop the loop when there's no parent package anymore
1005                         loop = False
1006                 else:
1007                     #stop the loop when the package test_pack is not totally reserved for moves of this picking
1008                     #(some quants may be reserved for other picking or not reserved at all)
1009                     loop = False
1010             if good_pack:
1011                 top_lvl_packages.add(good_pack)
1012         return list(top_lvl_packages)
1013
1014     def _prepare_pack_ops(self, cr, uid, picking, quants, forced_qties, context=None):
1015         """ returns a list of dict, ready to be used in create() of stock.pack.operation.
1016
1017         :param picking: browse record (stock.picking)
1018         :param quants: browse record list (stock.quant). List of quants associated to the picking
1019         :param forced_qties: dictionary showing for each product (keys) its corresponding quantity (value) that is not covered by the quants associated to the picking
1020         """
1021         def _picking_putaway_apply(product):
1022             location = False
1023             # Search putaway strategy
1024             if product_putaway_strats.get(product.id):
1025                 location = product_putaway_strats[product.id]
1026             else:
1027                 location = self.pool.get('stock.location').get_putaway_strategy(cr, uid, picking.location_dest_id, product, context=context)
1028                 product_putaway_strats[product.id] = location
1029             return location or picking.location_dest_id.id
1030
1031         # If we encounter an UoM that is smaller than the default UoM or the one already chosen, use the new one instead.
1032         product_uom = {} # Determines UoM used in pack operations
1033         for move in picking.move_lines:
1034             if not product_uom.get(move.product_id.id):
1035                 product_uom[move.product_id.id] = move.product_id.uom_id.id
1036             if move.product_uom.id != move.product_id.uom_id.id and move.product_uom.factor > product_uom[move.product_id.id]:
1037                 product_uom[move.product_id.id] = move.product_uom.id
1038
1039         pack_obj = self.pool.get("stock.quant.package")
1040         quant_obj = self.pool.get("stock.quant")
1041         vals = []
1042         qtys_grouped = {}
1043         #for each quant of the picking, find the suggested location
1044         quants_suggested_locations = {}
1045         product_putaway_strats = {}
1046         for quant in quants:
1047             if quant.qty <= 0:
1048                 continue
1049             suggested_location_id = _picking_putaway_apply(quant.product_id)
1050             quants_suggested_locations[quant] = suggested_location_id
1051
1052         #find the packages we can movei as a whole
1053         top_lvl_packages = self._get_top_level_packages(cr, uid, quants_suggested_locations, context=context)
1054         # and then create pack operations for the top-level packages found
1055         for pack in top_lvl_packages:
1056             pack_quant_ids = pack_obj.get_content(cr, uid, [pack.id], context=context)
1057             pack_quants = quant_obj.browse(cr, uid, pack_quant_ids, context=context)
1058             vals.append({
1059                     'picking_id': picking.id,
1060                     'package_id': pack.id,
1061                     'product_qty': 1.0,
1062                     'location_id': pack.location_id.id,
1063                     'location_dest_id': quants_suggested_locations[pack_quants[0]],
1064                 })
1065             #remove the quants inside the package so that they are excluded from the rest of the computation
1066             for quant in pack_quants:
1067                 del quants_suggested_locations[quant]
1068
1069         # Go through all remaining reserved quants and group by product, package, lot, owner, source location and dest location
1070         for quant, dest_location_id in quants_suggested_locations.items():
1071             key = (quant.product_id.id, quant.package_id.id, quant.lot_id.id, quant.owner_id.id, quant.location_id.id, dest_location_id)
1072             if qtys_grouped.get(key):
1073                 qtys_grouped[key] += quant.qty
1074             else:
1075                 qtys_grouped[key] = quant.qty
1076
1077         # Do the same for the forced quantities (in cases of force_assign or incomming shipment for example)
1078         for product, qty in forced_qties.items():
1079             if qty <= 0:
1080                 continue
1081             suggested_location_id = _picking_putaway_apply(product)
1082             key = (product.id, False, False, False, picking.location_id.id, suggested_location_id)
1083             if qtys_grouped.get(key):
1084                 qtys_grouped[key] += qty
1085             else:
1086                 qtys_grouped[key] = qty
1087
1088         # Create the necessary operations for the grouped quants and remaining qtys
1089         uom_obj = self.pool.get('product.uom')
1090         for key, qty in qtys_grouped.items():
1091             product = self.pool.get("product.product").browse(cr, uid, key[0], context=context)
1092             uom_id = product.uom_id.id
1093             qty_uom = qty
1094             if product_uom.get(key[0]):
1095                 uom_id = product_uom[key[0]]
1096                 qty_uom = uom_obj._compute_qty(cr, uid, product.uom_id.id, qty, uom_id)
1097             vals.append({
1098                 'picking_id': picking.id,
1099                 'product_qty': qty_uom,
1100                 'product_id': key[0],
1101                 'package_id': key[1],
1102                 'lot_id': key[2],
1103                 'owner_id': key[3],
1104                 'location_id': key[4],
1105                 'location_dest_id': key[5],
1106                 'product_uom_id': uom_id,
1107             })
1108         return vals
1109
1110     @api.cr_uid_ids_context
1111     def open_barcode_interface(self, cr, uid, picking_ids, context=None):
1112         final_url="/barcode/web/#action=stock.ui&picking_id="+str(picking_ids[0])
1113         return {'type': 'ir.actions.act_url', 'url':final_url, 'target': 'self',}
1114
1115     @api.cr_uid_ids_context
1116     def do_partial_open_barcode(self, cr, uid, picking_ids, context=None):
1117         self.do_prepare_partial(cr, uid, picking_ids, context=context)
1118         return self.open_barcode_interface(cr, uid, picking_ids, context=context)
1119
1120     @api.cr_uid_ids_context
1121     def do_prepare_partial(self, cr, uid, picking_ids, context=None):
1122         context = context or {}
1123         pack_operation_obj = self.pool.get('stock.pack.operation')
1124         #used to avoid recomputing the remaining quantities at each new pack operation created
1125         ctx = context.copy()
1126         ctx['no_recompute'] = True
1127
1128         #get list of existing operations and delete them
1129         existing_package_ids = pack_operation_obj.search(cr, uid, [('picking_id', 'in', picking_ids)], context=context)
1130         if existing_package_ids:
1131             pack_operation_obj.unlink(cr, uid, existing_package_ids, context)
1132         for picking in self.browse(cr, uid, picking_ids, context=context):
1133             forced_qties = {}  # Quantity remaining after calculating reserved quants
1134             picking_quants = []
1135             #Calculate packages, reserved quants, qtys of this picking's moves
1136             for move in picking.move_lines:
1137                 if move.state not in ('assigned', 'confirmed'):
1138                     continue
1139                 move_quants = move.reserved_quant_ids
1140                 picking_quants += move_quants
1141                 forced_qty = (move.state == 'assigned') and move.product_qty - sum([x.qty for x in move_quants]) or 0
1142                 #if we used force_assign() on the move, or if the move is incoming, forced_qty > 0
1143                 if float_compare(forced_qty, 0, precision_rounding=move.product_id.uom_id.rounding) > 0:
1144                     if forced_qties.get(move.product_id):
1145                         forced_qties[move.product_id] += forced_qty
1146                     else:
1147                         forced_qties[move.product_id] = forced_qty
1148             for vals in self._prepare_pack_ops(cr, uid, picking, picking_quants, forced_qties, context=context):
1149                 pack_operation_obj.create(cr, uid, vals, context=ctx)
1150         #recompute the remaining quantities all at once
1151         self.do_recompute_remaining_quantities(cr, uid, picking_ids, context=context)
1152         self.write(cr, uid, picking_ids, {'recompute_pack_op': False}, context=context)
1153
1154     @api.cr_uid_ids_context
1155     def do_unreserve(self, cr, uid, picking_ids, context=None):
1156         """
1157           Will remove all quants for picking in picking_ids
1158         """
1159         moves_to_unreserve = []
1160         pack_line_to_unreserve = []
1161         for picking in self.browse(cr, uid, picking_ids, context=context):
1162             moves_to_unreserve += [m.id for m in picking.move_lines if m.state not in ('done', 'cancel')]
1163             pack_line_to_unreserve += [p.id for p in picking.pack_operation_ids]
1164         if moves_to_unreserve:
1165             if pack_line_to_unreserve:
1166                 self.pool.get('stock.pack.operation').unlink(cr, uid, pack_line_to_unreserve, context=context)
1167             self.pool.get('stock.move').do_unreserve(cr, uid, moves_to_unreserve, context=context)
1168
1169     def recompute_remaining_qty(self, cr, uid, picking, context=None):
1170         def _create_link_for_index(operation_id, index, product_id, qty_to_assign, quant_id=False):
1171             move_dict = prod2move_ids[product_id][index]
1172             qty_on_link = min(move_dict['remaining_qty'], qty_to_assign)
1173             self.pool.get('stock.move.operation.link').create(cr, uid, {'move_id': move_dict['move'].id, 'operation_id': operation_id, 'qty': qty_on_link, 'reserved_quant_id': quant_id}, context=context)
1174             if move_dict['remaining_qty'] == qty_on_link:
1175                 prod2move_ids[product_id].pop(index)
1176             else:
1177                 move_dict['remaining_qty'] -= qty_on_link
1178             return qty_on_link
1179
1180         def _create_link_for_quant(operation_id, quant, qty):
1181             """create a link for given operation and reserved move of given quant, for the max quantity possible, and returns this quantity"""
1182             if not quant.reservation_id.id:
1183                 return _create_link_for_product(operation_id, quant.product_id.id, qty)
1184             qty_on_link = 0
1185             for i in range(0, len(prod2move_ids[quant.product_id.id])):
1186                 if prod2move_ids[quant.product_id.id][i]['move'].id != quant.reservation_id.id:
1187                     continue
1188                 qty_on_link = _create_link_for_index(operation_id, i, quant.product_id.id, qty, quant_id=quant.id)
1189                 break
1190             return qty_on_link
1191
1192         def _create_link_for_product(operation_id, product_id, qty):
1193             '''method that creates the link between a given operation and move(s) of given product, for the given quantity.
1194             Returns True if it was possible to create links for the requested quantity (False if there was not enough quantity on stock moves)'''
1195             qty_to_assign = qty
1196             prod_obj = self.pool.get("product.product")
1197             product = prod_obj.browse(cr, uid, product_id)
1198             rounding = product.uom_id.rounding
1199             qtyassign_cmp = float_compare(qty_to_assign, 0.0, precision_rounding=rounding)
1200             if prod2move_ids.get(product_id):
1201                 while prod2move_ids[product_id] and qtyassign_cmp > 0:
1202                     qty_on_link = _create_link_for_index(operation_id, 0, product_id, qty_to_assign, quant_id=False)
1203                     qty_to_assign -= qty_on_link
1204                     qtyassign_cmp = float_compare(qty_to_assign, 0.0, precision_rounding=rounding)
1205             return qtyassign_cmp == 0
1206
1207         uom_obj = self.pool.get('product.uom')
1208         package_obj = self.pool.get('stock.quant.package')
1209         quant_obj = self.pool.get('stock.quant')
1210         quants_in_package_done = set()
1211         prod2move_ids = {}
1212         still_to_do = []
1213         #make a dictionary giving for each product, the moves and related quantity that can be used in operation links
1214         for move in picking.move_lines:
1215             if not prod2move_ids.get(move.product_id.id):
1216                 prod2move_ids[move.product_id.id] = [{'move': move, 'remaining_qty': move.product_qty}]
1217             else:
1218                 prod2move_ids[move.product_id.id].append({'move': move, 'remaining_qty': move.product_qty})
1219
1220         need_rereserve = False
1221         #sort the operations in order to give higher priority to those with a package, then a serial number
1222         operations = picking.pack_operation_ids
1223         operations = sorted(operations, key=lambda x: ((x.package_id and not x.product_id) and -4 or 0) + (x.package_id and -2 or 0) + (x.lot_id and -1 or 0))
1224         #delete existing operations to start again from scratch
1225         cr.execute("DELETE FROM stock_move_operation_link WHERE operation_id in %s", (tuple([x.id for x in operations]),))
1226         #1) first, try to create links when quants can be identified without any doubt
1227         for ops in operations:
1228             #for each operation, create the links with the stock move by seeking on the matching reserved quants,
1229             #and deffer the operation if there is some ambiguity on the move to select
1230             if ops.package_id and not ops.product_id:
1231                 #entire package
1232                 quant_ids = package_obj.get_content(cr, uid, [ops.package_id.id], context=context)
1233                 for quant in quant_obj.browse(cr, uid, quant_ids, context=context):
1234                     remaining_qty_on_quant = quant.qty
1235                     if quant.reservation_id:
1236                         #avoid quants being counted twice
1237                         quants_in_package_done.add(quant.id)
1238                         qty_on_link = _create_link_for_quant(ops.id, quant, quant.qty)
1239                         remaining_qty_on_quant -= qty_on_link
1240                     if remaining_qty_on_quant:
1241                         still_to_do.append((ops, quant.product_id.id, remaining_qty_on_quant))
1242                         need_rereserve = True
1243             elif ops.product_id.id:
1244                 #Check moves with same product
1245                 qty_to_assign = uom_obj._compute_qty_obj(cr, uid, ops.product_uom_id, ops.product_qty, ops.product_id.uom_id, round=False, context=context)
1246                 for move_dict in prod2move_ids.get(ops.product_id.id, []):
1247                     move = move_dict['move']
1248                     for quant in move.reserved_quant_ids:
1249                         if not qty_to_assign > 0:
1250                             break
1251                         if quant.id in quants_in_package_done:
1252                             continue
1253
1254                         #check if the quant is matching the operation details
1255                         if ops.package_id:
1256                             flag = quant.package_id and bool(package_obj.search(cr, uid, [('id', 'child_of', [ops.package_id.id])], context=context)) or False
1257                         else:
1258                             flag = not quant.package_id.id
1259                         flag = flag and ((ops.lot_id and ops.lot_id.id == quant.lot_id.id) or not ops.lot_id)
1260                         flag = flag and (ops.owner_id.id == quant.owner_id.id)
1261                         if flag:
1262                             max_qty_on_link = min(quant.qty, qty_to_assign)
1263                             qty_on_link = _create_link_for_quant(ops.id, quant, max_qty_on_link)
1264                             qty_to_assign -= qty_on_link
1265                 qty_assign_cmp = float_compare(qty_to_assign, 0, precision_rounding=ops.product_id.uom_id.rounding)
1266                 if qty_assign_cmp > 0:
1267                     #qty reserved is less than qty put in operations. We need to create a link but it's deferred after we processed
1268                     #all the quants (because they leave no choice on their related move and needs to be processed with higher priority)
1269                     still_to_do += [(ops, ops.product_id.id, qty_to_assign)]
1270                     need_rereserve = True
1271
1272         #2) then, process the remaining part
1273         all_op_processed = True
1274         for ops, product_id, remaining_qty in still_to_do:
1275             all_op_processed = all_op_processed and _create_link_for_product(ops.id, product_id, remaining_qty)
1276         return (need_rereserve, all_op_processed)
1277
1278     def picking_recompute_remaining_quantities(self, cr, uid, picking, context=None):
1279         need_rereserve = False
1280         all_op_processed = True
1281         if picking.pack_operation_ids:
1282             need_rereserve, all_op_processed = self.recompute_remaining_qty(cr, uid, picking, context=context)
1283         return need_rereserve, all_op_processed
1284
1285     @api.cr_uid_ids_context
1286     def do_recompute_remaining_quantities(self, cr, uid, picking_ids, context=None):
1287         for picking in self.browse(cr, uid, picking_ids, context=context):
1288             if picking.pack_operation_ids:
1289                 self.recompute_remaining_qty(cr, uid, picking, context=context)
1290
1291     def _prepare_values_extra_move(self, cr, uid, op, product, remaining_qty, context=None):
1292         """
1293         Creates an extra move when there is no corresponding original move to be copied
1294         """
1295         uom_obj = self.pool.get("product.uom")
1296         uom_id = product.uom_id.id
1297         qty = remaining_qty
1298         if op.product_id and op.product_uom_id and op.product_uom_id.id != product.uom_id.id:
1299             if op.product_uom_id.factor > product.uom_id.factor: #If the pack operation's is a smaller unit
1300                 uom_id = op.product_uom_id.id
1301                 qty = uom_obj._compute_qty_obj(cr, uid, product.uom_id, remaining_qty, op.product_uom_id)
1302         picking = op.picking_id
1303         res = {
1304             'picking_id': picking.id,
1305             'location_id': picking.location_id.id,
1306             'location_dest_id': picking.location_dest_id.id,
1307             'product_id': product.id,
1308             'product_uom': uom_id,
1309             'product_uom_qty': qty,
1310             'name': _('Extra Move: ') + product.name,
1311             'state': 'draft',
1312             }
1313         return res
1314
1315     def _create_extra_moves(self, cr, uid, picking, context=None):
1316         '''This function creates move lines on a picking, at the time of do_transfer, based on
1317         unexpected product transfers (or exceeding quantities) found in the pack operations.
1318         '''
1319         move_obj = self.pool.get('stock.move')
1320         operation_obj = self.pool.get('stock.pack.operation')
1321         moves = []
1322         for op in picking.pack_operation_ids:
1323             for product_id, remaining_qty in operation_obj._get_remaining_prod_quantities(cr, uid, op, context=context).items():
1324                 product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
1325                 if float_compare(remaining_qty, 0, precision_rounding=product.uom_id.rounding) > 0:
1326                     vals = self._prepare_values_extra_move(cr, uid, op, product, remaining_qty, context=context)
1327                     moves.append(move_obj.create(cr, uid, vals, context=context))
1328         if moves:
1329             move_obj.action_confirm(cr, uid, moves, context=context)
1330         return moves
1331
1332     def rereserve_pick(self, cr, uid, ids, context=None):
1333         """
1334         This can be used to provide a button that rereserves taking into account the existing pack operations
1335         """
1336         for pick in self.browse(cr, uid, ids, context=context):
1337             self.rereserve_quants(cr, uid, pick, move_ids = [x.id for x in pick.move_lines], context=context)
1338
1339     def rereserve_quants(self, cr, uid, picking, move_ids=[], context=None):
1340         """ Unreserve quants then try to reassign quants."""
1341         stock_move_obj = self.pool.get('stock.move')
1342         if not move_ids:
1343             self.do_unreserve(cr, uid, [picking.id], context=context)
1344             self.action_assign(cr, uid, [picking.id], context=context)
1345         else:
1346             stock_move_obj.do_unreserve(cr, uid, move_ids, context=context)
1347             stock_move_obj.action_assign(cr, uid, move_ids, context=context)
1348
1349     @api.cr_uid_ids_context
1350     def do_enter_transfer_details(self, cr, uid, picking, context=None):
1351         if not context:
1352             context = {}
1353
1354         context.update({
1355             'active_model': self._name,
1356             'active_ids': picking,
1357             'active_id': len(picking) and picking[0] or False
1358         })
1359
1360         created_id = self.pool['stock.transfer_details'].create(cr, uid, {'picking_id': len(picking) and picking[0] or False}, context)
1361         return self.pool['stock.transfer_details'].wizard_view(cr, uid, created_id, context)
1362
1363
1364     @api.cr_uid_ids_context
1365     def do_transfer(self, cr, uid, picking_ids, context=None):
1366         """
1367             If no pack operation, we do simple action_done of the picking
1368             Otherwise, do the pack operations
1369         """
1370         if not context:
1371             context = {}
1372         stock_move_obj = self.pool.get('stock.move')
1373         for picking in self.browse(cr, uid, picking_ids, context=context):
1374             if not picking.pack_operation_ids:
1375                 self.action_done(cr, uid, [picking.id], context=context)
1376                 continue
1377             else:
1378                 need_rereserve, all_op_processed = self.picking_recompute_remaining_quantities(cr, uid, picking, context=context)
1379                 #create extra moves in the picking (unexpected product moves coming from pack operations)
1380                 todo_move_ids = []
1381                 if not all_op_processed:
1382                     todo_move_ids += self._create_extra_moves(cr, uid, picking, context=context)
1383
1384                 picking.refresh()
1385                 #split move lines eventually
1386
1387                 toassign_move_ids = []
1388                 for move in picking.move_lines:
1389                     remaining_qty = move.remaining_qty
1390                     if move.state in ('done', 'cancel'):
1391                         #ignore stock moves cancelled or already done
1392                         continue
1393                     elif move.state == 'draft':
1394                         toassign_move_ids.append(move.id)
1395                     if float_compare(remaining_qty, 0,  precision_rounding = move.product_id.uom_id.rounding) == 0:
1396                         if move.state in ('draft', 'assigned', 'confirmed'):
1397                             todo_move_ids.append(move.id)
1398                     elif float_compare(remaining_qty,0, precision_rounding = move.product_id.uom_id.rounding) > 0 and \
1399                                 float_compare(remaining_qty, move.product_qty, precision_rounding = move.product_id.uom_id.rounding) < 0:
1400                         new_move = stock_move_obj.split(cr, uid, move, remaining_qty, context=context)
1401                         todo_move_ids.append(move.id)
1402                         #Assign move as it was assigned before
1403                         toassign_move_ids.append(new_move)
1404                 if need_rereserve or not all_op_processed: 
1405                     if not picking.location_id.usage in ("supplier", "production", "inventory"):
1406                         self.rereserve_quants(cr, uid, picking, move_ids=todo_move_ids, context=context)
1407                     self.do_recompute_remaining_quantities(cr, uid, [picking.id], context=context)
1408                 if todo_move_ids and not context.get('do_only_split'):
1409                     self.pool.get('stock.move').action_done(cr, uid, todo_move_ids, context=context)
1410                 elif context.get('do_only_split'):
1411                     context = dict(context, split=todo_move_ids)
1412             picking.refresh()
1413             self._create_backorder(cr, uid, picking, context=context)
1414             if toassign_move_ids:
1415                 stock_move_obj.action_assign(cr, uid, toassign_move_ids, context=context)
1416         return True
1417
1418     @api.cr_uid_ids_context
1419     def do_split(self, cr, uid, picking_ids, context=None):
1420         """ just split the picking (create a backorder) without making it 'done' """
1421         if context is None:
1422             context = {}
1423         ctx = context.copy()
1424         ctx['do_only_split'] = True
1425         return self.do_transfer(cr, uid, picking_ids, context=ctx)
1426
1427     def get_next_picking_for_ui(self, cr, uid, context=None):
1428         """ returns the next pickings to process. Used in the barcode scanner UI"""
1429         if context is None:
1430             context = {}
1431         domain = [('state', 'in', ('assigned', 'partially_available'))]
1432         if context.get('default_picking_type_id'):
1433             domain.append(('picking_type_id', '=', context['default_picking_type_id']))
1434         return self.search(cr, uid, domain, context=context)
1435
1436     def action_done_from_ui(self, cr, uid, picking_id, context=None):
1437         """ called when button 'done' is pushed in the barcode scanner UI """
1438         #write qty_done into field product_qty for every package_operation before doing the transfer
1439         pack_op_obj = self.pool.get('stock.pack.operation')
1440         for operation in self.browse(cr, uid, picking_id, context=context).pack_operation_ids:
1441             pack_op_obj.write(cr, uid, operation.id, {'product_qty': operation.qty_done}, context=context)
1442         self.do_transfer(cr, uid, [picking_id], context=context)
1443         #return id of next picking to work on
1444         return self.get_next_picking_for_ui(cr, uid, context=context)
1445
1446     @api.cr_uid_ids_context
1447     def action_pack(self, cr, uid, picking_ids, operation_filter_ids=None, context=None):
1448         """ Create a package with the current pack_operation_ids of the picking that aren't yet in a pack.
1449         Used in the barcode scanner UI and the normal interface as well. 
1450         operation_filter_ids is used by barcode scanner interface to specify a subset of operation to pack"""
1451         if operation_filter_ids == None:
1452             operation_filter_ids = []
1453         stock_operation_obj = self.pool.get('stock.pack.operation')
1454         package_obj = self.pool.get('stock.quant.package')
1455         stock_move_obj = self.pool.get('stock.move')
1456         for picking_id in picking_ids:
1457             operation_search_domain = [('picking_id', '=', picking_id), ('result_package_id', '=', False)]
1458             if operation_filter_ids != []:
1459                 operation_search_domain.append(('id', 'in', operation_filter_ids))
1460             operation_ids = stock_operation_obj.search(cr, uid, operation_search_domain, context=context)
1461             pack_operation_ids = []
1462             if operation_ids:
1463                 for operation in stock_operation_obj.browse(cr, uid, operation_ids, context=context):
1464                     #If we haven't done all qty in operation, we have to split into 2 operation
1465                     op = operation
1466                     if (operation.qty_done < operation.product_qty):
1467                         new_operation = stock_operation_obj.copy(cr, uid, operation.id, {'product_qty': operation.qty_done,'qty_done': operation.qty_done}, context=context)
1468                         stock_operation_obj.write(cr, uid, operation.id, {'product_qty': operation.product_qty - operation.qty_done,'qty_done': 0, 'lot_id': False}, context=context)
1469                         op = stock_operation_obj.browse(cr, uid, new_operation, context=context)
1470                     pack_operation_ids.append(op.id)
1471                     if op.product_id and op.location_id and op.location_dest_id:
1472                         stock_move_obj.check_tracking_product(cr, uid, op.product_id, op.lot_id.id, op.location_id, op.location_dest_id, context=context)
1473                 package_id = package_obj.create(cr, uid, {}, context=context)
1474                 stock_operation_obj.write(cr, uid, pack_operation_ids, {'result_package_id': package_id}, context=context)
1475         return True
1476
1477     def process_product_id_from_ui(self, cr, uid, picking_id, product_id, op_id, increment=True, context=None):
1478         return self.pool.get('stock.pack.operation')._search_and_increment(cr, uid, picking_id, [('product_id', '=', product_id),('id', '=', op_id)], increment=increment, context=context)
1479
1480     def process_barcode_from_ui(self, cr, uid, picking_id, barcode_str, visible_op_ids, context=None):
1481         '''This function is called each time there barcode scanner reads an input'''
1482         lot_obj = self.pool.get('stock.production.lot')
1483         package_obj = self.pool.get('stock.quant.package')
1484         product_obj = self.pool.get('product.product')
1485         stock_operation_obj = self.pool.get('stock.pack.operation')
1486         stock_location_obj = self.pool.get('stock.location')
1487         answer = {'filter_loc': False, 'operation_id': False}
1488         #check if the barcode correspond to a location
1489         matching_location_ids = stock_location_obj.search(cr, uid, [('loc_barcode', '=', barcode_str)], context=context)
1490         if matching_location_ids:
1491             #if we have a location, return immediatly with the location name
1492             location = stock_location_obj.browse(cr, uid, matching_location_ids[0], context=None)
1493             answer['filter_loc'] = stock_location_obj._name_get(cr, uid, location, context=None)
1494             answer['filter_loc_id'] = matching_location_ids[0]
1495             return answer
1496         #check if the barcode correspond to a product
1497         matching_product_ids = product_obj.search(cr, uid, ['|', ('ean13', '=', barcode_str), ('default_code', '=', barcode_str)], context=context)
1498         if matching_product_ids:
1499             op_id = stock_operation_obj._search_and_increment(cr, uid, picking_id, [('product_id', '=', matching_product_ids[0])], filter_visible=True, visible_op_ids=visible_op_ids, increment=True, context=context)
1500             answer['operation_id'] = op_id
1501             return answer
1502         #check if the barcode correspond to a lot
1503         matching_lot_ids = lot_obj.search(cr, uid, [('name', '=', barcode_str)], context=context)
1504         if matching_lot_ids:
1505             lot = lot_obj.browse(cr, uid, matching_lot_ids[0], context=context)
1506             op_id = stock_operation_obj._search_and_increment(cr, uid, picking_id, [('product_id', '=', lot.product_id.id), ('lot_id', '=', lot.id)], filter_visible=True, visible_op_ids=visible_op_ids, increment=True, context=context)
1507             answer['operation_id'] = op_id
1508             return answer
1509         #check if the barcode correspond to a package
1510         matching_package_ids = package_obj.search(cr, uid, [('name', '=', barcode_str)], context=context)
1511         if matching_package_ids:
1512             op_id = stock_operation_obj._search_and_increment(cr, uid, picking_id, [('package_id', '=', matching_package_ids[0])], filter_visible=True, visible_op_ids=visible_op_ids, increment=True, context=context)
1513             answer['operation_id'] = op_id
1514             return answer
1515         return answer
1516
1517
1518 class stock_production_lot(osv.osv):
1519     _name = 'stock.production.lot'
1520     _inherit = ['mail.thread']
1521     _description = 'Lot/Serial'
1522     _columns = {
1523         'name': fields.char('Serial Number', required=True, help="Unique Serial Number"),
1524         'ref': fields.char('Internal Reference', help="Internal reference number in case it differs from the manufacturer's serial number"),
1525         'product_id': fields.many2one('product.product', 'Product', required=True, domain=[('type', '<>', 'service')]),
1526         'quant_ids': fields.one2many('stock.quant', 'lot_id', 'Quants', readonly=True),
1527         'create_date': fields.datetime('Creation Date'),
1528     }
1529     _defaults = {
1530         'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'stock.lot.serial'),
1531         'product_id': lambda x, y, z, c: c.get('product_id', False),
1532     }
1533     _sql_constraints = [
1534         ('name_ref_uniq', 'unique (name, ref, product_id)', 'The combination of serial number, internal reference and product must be unique !'),
1535     ]
1536
1537     def action_traceability(self, cr, uid, ids, context=None):
1538         """ It traces the information of lots
1539         @param self: The object pointer.
1540         @param cr: A database cursor
1541         @param uid: ID of the user currently logged in
1542         @param ids: List of IDs selected
1543         @param context: A standard dictionary
1544         @return: A dictionary of values
1545         """
1546         quant_obj = self.pool.get("stock.quant")
1547         quants = quant_obj.search(cr, uid, [('lot_id', 'in', ids)], context=context)
1548         moves = set()
1549         for quant in quant_obj.browse(cr, uid, quants, context=context):
1550             moves |= {move.id for move in quant.history_ids}
1551         if moves:
1552             return {
1553                 'domain': "[('id','in',[" + ','.join(map(str, list(moves))) + "])]",
1554                 'name': _('Traceability'),
1555                 'view_mode': 'tree,form',
1556                 'view_type': 'form',
1557                 'context': {'tree_view_ref': 'stock.view_move_tree'},
1558                 'res_model': 'stock.move',
1559                 'type': 'ir.actions.act_window',
1560                     }
1561         return False
1562
1563
1564 # ----------------------------------------------------
1565 # Move
1566 # ----------------------------------------------------
1567
1568 class stock_move(osv.osv):
1569     _name = "stock.move"
1570     _description = "Stock Move"
1571     _order = 'date_expected desc, id'
1572     _log_create = False
1573
1574     def get_price_unit(self, cr, uid, move, context=None):
1575         """ Returns the unit price to store on the quant """
1576         return move.price_unit or move.product_id.standard_price
1577
1578     def name_get(self, cr, uid, ids, context=None):
1579         res = []
1580         for line in self.browse(cr, uid, ids, context=context):
1581             name = line.location_id.name + ' > ' + line.location_dest_id.name
1582             if line.product_id.code:
1583                 name = line.product_id.code + ': ' + name
1584             if line.picking_id.origin:
1585                 name = line.picking_id.origin + '/ ' + name
1586             res.append((line.id, name))
1587         return res
1588
1589     def _quantity_normalize(self, cr, uid, ids, name, args, context=None):
1590         uom_obj = self.pool.get('product.uom')
1591         res = {}
1592         for m in self.browse(cr, uid, ids, context=context):
1593             res[m.id] = uom_obj._compute_qty_obj(cr, uid, m.product_uom, m.product_uom_qty, m.product_id.uom_id, context=context)
1594         return res
1595
1596     def _get_remaining_qty(self, cr, uid, ids, field_name, args, context=None):
1597         uom_obj = self.pool.get('product.uom')
1598         res = {}
1599         for move in self.browse(cr, uid, ids, context=context):
1600             qty = move.product_qty
1601             for record in move.linked_move_operation_ids:
1602                 qty -= record.qty
1603             # Keeping in product default UoM
1604             res[move.id] = qty
1605         return res
1606
1607     def _get_lot_ids(self, cr, uid, ids, field_name, args, context=None):
1608         res = dict.fromkeys(ids, False)
1609         for move in self.browse(cr, uid, ids, context=context):
1610             if move.state == 'done':
1611                 res[move.id] = [q.lot_id.id for q in move.quant_ids if q.lot_id]
1612             else:
1613                 res[move.id] = [q.lot_id.id for q in move.reserved_quant_ids if q.lot_id]
1614         return res
1615
1616     def _get_product_availability(self, cr, uid, ids, field_name, args, context=None):
1617         quant_obj = self.pool.get('stock.quant')
1618         res = dict.fromkeys(ids, False)
1619         for move in self.browse(cr, uid, ids, context=context):
1620             if move.state == 'done':
1621                 res[move.id] = move.product_qty
1622             else:
1623                 sublocation_ids = self.pool.get('stock.location').search(cr, uid, [('id', 'child_of', [move.location_id.id])], context=context)
1624                 quant_ids = quant_obj.search(cr, uid, [('location_id', 'in', sublocation_ids), ('product_id', '=', move.product_id.id), ('reservation_id', '=', False)], context=context)
1625                 availability = 0
1626                 for quant in quant_obj.browse(cr, uid, quant_ids, context=context):
1627                     availability += quant.qty
1628                 res[move.id] = min(move.product_qty, availability)
1629         return res
1630
1631     def _get_string_qty_information(self, cr, uid, ids, field_name, args, context=None):
1632         settings_obj = self.pool.get('stock.config.settings')
1633         uom_obj = self.pool.get('product.uom')
1634         res = dict.fromkeys(ids, '')
1635         for move in self.browse(cr, uid, ids, context=context):
1636             if move.state in ('draft', 'done', 'cancel') or move.location_id.usage != 'internal':
1637                 res[move.id] = ''  # 'not applicable' or 'n/a' could work too
1638                 continue
1639             total_available = min(move.product_qty, move.reserved_availability + move.availability)
1640             total_available = uom_obj._compute_qty_obj(cr, uid, move.product_id.uom_id, total_available, move.product_uom, context=context)
1641             info = str(total_available)
1642             #look in the settings if we need to display the UoM name or not
1643             config_ids = settings_obj.search(cr, uid, [], limit=1, order='id DESC', context=context)
1644             if config_ids:
1645                 stock_settings = settings_obj.browse(cr, uid, config_ids[0], context=context)
1646                 if stock_settings.group_uom:
1647                     info += ' ' + move.product_uom.name
1648             if move.reserved_availability:
1649                 if move.reserved_availability != total_available:
1650                     #some of the available quantity is assigned and some are available but not reserved
1651                     reserved_available = uom_obj._compute_qty_obj(cr, uid, move.product_id.uom_id, move.reserved_availability, move.product_uom, context=context)
1652                     info += _(' (%s reserved)') % str(reserved_available)
1653                 else:
1654                     #all available quantity is assigned
1655                     info += _(' (reserved)')
1656             res[move.id] = info
1657         return res
1658
1659     def _get_reserved_availability(self, cr, uid, ids, field_name, args, context=None):
1660         res = dict.fromkeys(ids, 0)
1661         for move in self.browse(cr, uid, ids, context=context):
1662             res[move.id] = sum([quant.qty for quant in move.reserved_quant_ids])
1663         return res
1664
1665     def _get_move(self, cr, uid, ids, context=None):
1666         res = set()
1667         for quant in self.browse(cr, uid, ids, context=context):
1668             if quant.reservation_id:
1669                 res.add(quant.reservation_id.id)
1670         return list(res)
1671
1672     def _get_move_ids(self, cr, uid, ids, context=None):
1673         res = []
1674         for picking in self.browse(cr, uid, ids, context=context):
1675             res += [x.id for x in picking.move_lines]
1676         return res
1677
1678     def _get_moves_from_prod(self, cr, uid, ids, context=None):
1679         if ids:
1680             return self.pool.get('stock.move').search(cr, uid, [('product_id', 'in', ids)], context=context)
1681         return []
1682
1683     def _set_product_qty(self, cr, uid, id, field, value, arg, context=None):
1684         """ The meaning of product_qty field changed lately and is now a functional field computing the quantity
1685             in the default product UoM. This code has been added to raise an error if a write is made given a value
1686             for `product_qty`, where the same write should set the `product_uom_qty` field instead, in order to
1687             detect errors.
1688         """
1689         raise osv.except_osv(_('Programming Error!'), _('The requested operation cannot be processed because of a programming error setting the `product_qty` field instead of the `product_uom_qty`.'))
1690
1691     _columns = {
1692         'name': fields.char('Description', required=True, select=True),
1693         'priority': fields.selection(procurement.PROCUREMENT_PRIORITIES, 'Priority'),
1694         'create_date': fields.datetime('Creation Date', readonly=True, select=True),
1695         'date': fields.datetime('Date', required=True, select=True, help="Move date: scheduled date until move is done, then date of actual move processing", states={'done': [('readonly', True)]}),
1696         'date_expected': fields.datetime('Expected Date', states={'done': [('readonly', True)]}, required=True, select=True, help="Scheduled date for the processing of this move"),
1697         'product_id': fields.many2one('product.product', 'Product', required=True, select=True, domain=[('type', '<>', 'service')], states={'done': [('readonly', True)]}),
1698         'product_qty': fields.function(_quantity_normalize, fnct_inv=_set_product_qty, type='float', digits=0, store={
1699                 'stock.move': (lambda self, cr, uid, ids, ctx: ids, ['product_id', 'product_uom_qty', 'product_uom'], 20),
1700                 'product.product': (_get_moves_from_prod, ['uom_id'], 20),
1701             }, string='Quantity',
1702             help='Quantity in the default UoM of the product'),
1703         'product_uom_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'),
1704             required=True, states={'done': [('readonly', True)]},
1705             help="This is the quantity of products from an inventory "
1706                 "point of view. For moves in the state 'done', this is the "
1707                 "quantity of products that were actually moved. For other "
1708                 "moves, this is the quantity of product that is planned to "
1709                 "be moved. Lowering this quantity does not generate a "
1710                 "backorder. Changing this quantity on assigned moves affects "
1711                 "the product reservation, and should be done with care."
1712         ),
1713         'product_uom': fields.many2one('product.uom', 'Unit of Measure', required=True, states={'done': [('readonly', True)]}),
1714         'product_uos_qty': fields.float('Quantity (UOS)', digits_compute=dp.get_precision('Product Unit of Measure'), states={'done': [('readonly', True)]}),
1715         'product_uos': fields.many2one('product.uom', 'Product UOS', states={'done': [('readonly', True)]}),
1716
1717         'product_packaging': fields.many2one('product.packaging', 'Prefered Packaging', help="It specifies attributes of packaging like type, quantity of packaging,etc."),
1718
1719         'location_id': fields.many2one('stock.location', 'Source Location', required=True, select=True, states={'done': [('readonly', True)]}, help="Sets a location if you produce at a fixed location. This can be a partner location if you subcontract the manufacturing operations."),
1720         'location_dest_id': fields.many2one('stock.location', 'Destination Location', required=True, states={'done': [('readonly', True)]}, select=True, help="Location where the system will stock the finished products."),
1721
1722         'partner_id': fields.many2one('res.partner', 'Destination Address ', states={'done': [('readonly', True)]}, help="Optional address where goods are to be delivered, specifically used for allotment"),
1723
1724
1725         'move_dest_id': fields.many2one('stock.move', 'Destination Move', help="Optional: next stock move when chaining them", select=True, copy=False),
1726         'move_orig_ids': fields.one2many('stock.move', 'move_dest_id', 'Original Move', help="Optional: previous stock move when chaining them", select=True),
1727
1728         'picking_id': fields.many2one('stock.picking', 'Reference', select=True, states={'done': [('readonly', True)]}),
1729         'note': fields.text('Notes'),
1730         'state': fields.selection([('draft', 'New'),
1731                                    ('cancel', 'Cancelled'),
1732                                    ('waiting', 'Waiting Another Move'),
1733                                    ('confirmed', 'Waiting Availability'),
1734                                    ('assigned', 'Available'),
1735                                    ('done', 'Done'),
1736                                    ], 'Status', readonly=True, select=True, copy=False,
1737                  help= "* New: When the stock move is created and not yet confirmed.\n"\
1738                        "* Waiting Another Move: This state can be seen when a move is waiting for another one, for example in a chained flow.\n"\
1739                        "* Waiting Availability: This state is reached when the procurement resolution is not straight forward. It may need the scheduler to run, a component to me manufactured...\n"\
1740                        "* Available: When products are reserved, it is set to \'Available\'.\n"\
1741                        "* Done: When the shipment is processed, the state is \'Done\'."),
1742         'partially_available': fields.boolean('Partially Available', readonly=True, help="Checks if the move has some stock reserved", copy=False),
1743         '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
1744
1745         'company_id': fields.many2one('res.company', 'Company', required=True, select=True),
1746         '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", copy=False),
1747         'backorder_id': fields.related('picking_id', 'backorder_id', type='many2one', relation="stock.picking", string="Back Order of", select=True),
1748         'origin': fields.char("Source"),
1749         'procure_method': fields.selection([('make_to_stock', 'Default: Take From Stock'), ('make_to_order', 'Advanced: Apply Procurement Rules')], 'Supply Method', required=True, 
1750                                            help="""By default, the system will take from the stock in the source location and passively wait for availability. The other possibility allows you to directly create a procurement on the source location (and thus ignore its current stock) to gather products. If we want to chain moves and have this one to wait for the previous, this second option should be chosen."""),
1751
1752         # used for colors in tree views:
1753         'scrapped': fields.related('location_dest_id', 'scrap_location', type='boolean', relation='stock.location', string='Scrapped', readonly=True),
1754
1755         'quant_ids': fields.many2many('stock.quant', 'stock_quant_move_rel', 'move_id', 'quant_id', 'Moved Quants'),
1756         'reserved_quant_ids': fields.one2many('stock.quant', 'reservation_id', 'Reserved quants'),
1757         'linked_move_operation_ids': fields.one2many('stock.move.operation.link', 'move_id', string='Linked Operations', readonly=True, help='Operations that impact this move for the computation of the remaining quantities'),
1758         'remaining_qty': fields.function(_get_remaining_qty, type='float', string='Remaining Quantity', digits=0,
1759                                          states={'done': [('readonly', True)]}, help="Remaining Quantity in default UoM according to operations matched with this move"),
1760         'procurement_id': fields.many2one('procurement.order', 'Procurement'),
1761         'group_id': fields.many2one('procurement.group', 'Procurement Group'),
1762         'rule_id': fields.many2one('procurement.rule', 'Procurement Rule', help='The pull rule that created this stock move'),
1763         'push_rule_id': fields.many2one('stock.location.path', 'Push Rule', help='The push rule that created this stock move'),
1764         'propagate': fields.boolean('Propagate cancel and split', help='If checked, when this move is cancelled, cancel the linked move too'),
1765         'picking_type_id': fields.many2one('stock.picking.type', 'Picking Type'),
1766         'inventory_id': fields.many2one('stock.inventory', 'Inventory'),
1767         'lot_ids': fields.function(_get_lot_ids, type='many2many', relation='stock.production.lot', string='Lots'),
1768         'origin_returned_move_id': fields.many2one('stock.move', 'Origin return move', help='move that created the return move', copy=False),
1769         'returned_move_ids': fields.one2many('stock.move', 'origin_returned_move_id', 'All returned moves', help='Optional: all returned moves created from this move'),
1770         'reserved_availability': fields.function(_get_reserved_availability, type='float', string='Quantity Reserved', readonly=True, help='Quantity that has already been reserved for this move'),
1771         'availability': fields.function(_get_product_availability, type='float', string='Quantity Available', readonly=True, help='Quantity in stock that can still be reserved for this move'),
1772         'string_availability_info': fields.function(_get_string_qty_information, type='text', string='Availability', readonly=True, help='Show various information on stock availability for this move'),
1773         'restrict_lot_id': fields.many2one('stock.production.lot', 'Lot', help="Technical field used to depict a restriction on the lot of quants to consider when marking this move as 'done'"),
1774         'restrict_partner_id': fields.many2one('res.partner', 'Owner ', help="Technical field used to depict a restriction on the ownership of quants to consider when marking this move as 'done'"),
1775         'route_ids': fields.many2many('stock.location.route', 'stock_location_route_move', 'move_id', 'route_id', 'Destination route', help="Preferred route to be followed by the procurement order"),
1776         'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', help="Technical field depicting the warehouse to consider for the route selection on the next procurement (if any)."),
1777     }
1778
1779     def _default_location_destination(self, cr, uid, context=None):
1780         context = context or {}
1781         if context.get('default_picking_type_id', False):
1782             pick_type = self.pool.get('stock.picking.type').browse(cr, uid, context['default_picking_type_id'], context=context)
1783             return pick_type.default_location_dest_id and pick_type.default_location_dest_id.id or False
1784         return False
1785
1786     def _default_location_source(self, cr, uid, context=None):
1787         context = context or {}
1788         if context.get('default_picking_type_id', False):
1789             pick_type = self.pool.get('stock.picking.type').browse(cr, uid, context['default_picking_type_id'], context=context)
1790             return pick_type.default_location_src_id and pick_type.default_location_src_id.id or False
1791         return False
1792
1793     def _default_destination_address(self, cr, uid, context=None):
1794         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
1795         return user.company_id.partner_id.id
1796
1797     _defaults = {
1798         'location_id': _default_location_source,
1799         'location_dest_id': _default_location_destination,
1800         'partner_id': _default_destination_address,
1801         'state': 'draft',
1802         'priority': '1',
1803         'product_uom_qty': 1.0,
1804         'scrapped': False,
1805         'date': fields.datetime.now,
1806         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.move', context=c),
1807         'date_expected': fields.datetime.now,
1808         'procure_method': 'make_to_stock',
1809         'propagate': True,
1810         'partially_available': False,
1811     }
1812
1813     def _check_uom(self, cr, uid, ids, context=None):
1814         for move in self.browse(cr, uid, ids, context=context):
1815             if move.product_id.uom_id.category_id.id != move.product_uom.category_id.id:
1816                 return False
1817         return True
1818
1819     _constraints = [
1820         (_check_uom,
1821             'You try to move a product using a UoM that is not compatible with the UoM of the product moved. Please use an UoM in the same UoM category.',
1822             ['product_uom']),
1823     ]
1824
1825     @api.cr_uid_ids_context
1826     def do_unreserve(self, cr, uid, move_ids, context=None):
1827         quant_obj = self.pool.get("stock.quant")
1828         for move in self.browse(cr, uid, move_ids, context=context):
1829             if move.state in ('done', 'cancel'):
1830                 raise osv.except_osv(_('Operation Forbidden!'), _('Cannot unreserve a done move'))
1831             quant_obj.quants_unreserve(cr, uid, move, context=context)
1832             if self.find_move_ancestors(cr, uid, move, context=context):
1833                 self.write(cr, uid, [move.id], {'state': 'waiting'}, context=context)
1834             else:
1835                 self.write(cr, uid, [move.id], {'state': 'confirmed'}, context=context)
1836
1837     def _prepare_procurement_from_move(self, cr, uid, move, context=None):
1838         origin = (move.group_id and (move.group_id.name + ":") or "") + (move.rule_id and move.rule_id.name or move.origin or "/")
1839         group_id = move.group_id and move.group_id.id or False
1840         if move.rule_id:
1841             if move.rule_id.group_propagation_option == 'fixed' and move.rule_id.group_id:
1842                 group_id = move.rule_id.group_id.id
1843             elif move.rule_id.group_propagation_option == 'none':
1844                 group_id = False
1845         return {
1846             'name': move.rule_id and move.rule_id.name or "/",
1847             'origin': origin,
1848             'company_id': move.company_id and move.company_id.id or False,
1849             'date_planned': move.date,
1850             'product_id': move.product_id.id,
1851             'product_qty': move.product_qty,
1852             'product_uom': move.product_uom.id,
1853             'product_uos_qty': (move.product_uos and move.product_uos_qty) or move.product_qty,
1854             'product_uos': (move.product_uos and move.product_uos.id) or move.product_uom.id,
1855             'location_id': move.location_id.id,
1856             'move_dest_id': move.id,
1857             'group_id': group_id,
1858             'route_ids': [(4, x.id) for x in move.route_ids],
1859             'warehouse_id': move.warehouse_id.id or (move.picking_type_id and move.picking_type_id.warehouse_id.id or False),
1860             'priority': move.priority,
1861         }
1862
1863     def _push_apply(self, cr, uid, moves, context=None):
1864         push_obj = self.pool.get("stock.location.path")
1865         for move in moves:
1866             #1) if the move is already chained, there is no need to check push rules
1867             #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
1868             #   to receive goods without triggering the push rules again (which would duplicate chained operations)
1869             if not move.move_dest_id and not move.origin_returned_move_id:
1870                 domain = [('location_from_id', '=', move.location_dest_id.id)]
1871                 #priority goes to the route defined on the product and product category
1872                 route_ids = [x.id for x in move.product_id.route_ids + move.product_id.categ_id.total_route_ids]
1873                 rules = push_obj.search(cr, uid, domain + [('route_id', 'in', route_ids)], order='route_sequence, sequence', context=context)
1874                 if not rules:
1875                     #then we search on the warehouse if a rule can apply
1876                     wh_route_ids = []
1877                     if move.warehouse_id:
1878                         wh_route_ids = [x.id for x in move.warehouse_id.route_ids]
1879                     elif move.picking_type_id and move.picking_type_id.warehouse_id:
1880                         wh_route_ids = [x.id for x in move.picking_type_id.warehouse_id.route_ids]
1881                     if wh_route_ids:
1882                         rules = push_obj.search(cr, uid, domain + [('route_id', 'in', wh_route_ids)], order='route_sequence, sequence', context=context)
1883                     if not rules:
1884                         #if no specialized push rule has been found yet, we try to find a general one (without route)
1885                         rules = push_obj.search(cr, uid, domain + [('route_id', '=', False)], order='sequence', context=context)
1886                 if rules:
1887                     rule = push_obj.browse(cr, uid, rules[0], context=context)
1888                     push_obj._apply(cr, uid, rule, move, context=context)
1889         return True
1890
1891     def _create_procurement(self, cr, uid, move, context=None):
1892         """ This will create a procurement order """
1893         return self.pool.get("procurement.order").create(cr, uid, self._prepare_procurement_from_move(cr, uid, move, context=context))
1894
1895     def write(self, cr, uid, ids, vals, context=None):
1896         if context is None:
1897             context = {}
1898         if isinstance(ids, (int, long)):
1899             ids = [ids]
1900         # Check that we do not modify a stock.move which is done
1901         frozen_fields = set(['product_qty', 'product_uom', 'product_uos_qty', 'product_uos', 'location_id', 'location_dest_id', 'product_id'])
1902         for move in self.browse(cr, uid, ids, context=context):
1903             if move.state == 'done':
1904                 if frozen_fields.intersection(vals):
1905                     raise osv.except_osv(_('Operation Forbidden!'),
1906                         _('Quantities, Units of Measure, Products and Locations cannot be modified on stock moves that have already been processed (except by the Administrator).'))
1907         propagated_changes_dict = {}
1908         #propagation of quantity change
1909         if vals.get('product_uom_qty'):
1910             propagated_changes_dict['product_uom_qty'] = vals['product_uom_qty']
1911         if vals.get('product_uom_id'):
1912             propagated_changes_dict['product_uom_id'] = vals['product_uom_id']
1913         #propagation of expected date:
1914         propagated_date_field = False
1915         if vals.get('date_expected'):
1916             #propagate any manual change of the expected date
1917             propagated_date_field = 'date_expected'
1918         elif (vals.get('state', '') == 'done' and vals.get('date')):
1919             #propagate also any delta observed when setting the move as done
1920             propagated_date_field = 'date'
1921
1922         if not context.get('do_not_propagate', False) and (propagated_date_field or propagated_changes_dict):
1923             #any propagation is (maybe) needed
1924             for move in self.browse(cr, uid, ids, context=context):
1925                 if move.move_dest_id and move.propagate:
1926                     if 'date_expected' in propagated_changes_dict:
1927                         propagated_changes_dict.pop('date_expected')
1928                     if propagated_date_field:
1929                         current_date = datetime.strptime(move.date_expected, DEFAULT_SERVER_DATETIME_FORMAT)
1930                         new_date = datetime.strptime(vals.get(propagated_date_field), DEFAULT_SERVER_DATETIME_FORMAT)
1931                         delta = new_date - current_date
1932                         if abs(delta.days) >= move.company_id.propagation_minimum_delta:
1933                             old_move_date = datetime.strptime(move.move_dest_id.date_expected, DEFAULT_SERVER_DATETIME_FORMAT)
1934                             new_move_date = (old_move_date + relativedelta.relativedelta(days=delta.days or 0)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
1935                             propagated_changes_dict['date_expected'] = new_move_date
1936                     #For pushed moves as well as for pulled moves, propagate by recursive call of write().
1937                     #Note that, for pulled moves we intentionally don't propagate on the procurement.
1938                     if propagated_changes_dict:
1939                         self.write(cr, uid, [move.move_dest_id.id], propagated_changes_dict, context=context)
1940         return super(stock_move, self).write(cr, uid, ids, vals, context=context)
1941
1942     def onchange_quantity(self, cr, uid, ids, product_id, product_qty, product_uom, product_uos):
1943         """ On change of product quantity finds UoM and UoS quantities
1944         @param product_id: Product id
1945         @param product_qty: Changed Quantity of product
1946         @param product_uom: Unit of measure of product
1947         @param product_uos: Unit of sale of product
1948         @return: Dictionary of values
1949         """
1950         result = {
1951             'product_uos_qty': 0.00
1952         }
1953         warning = {}
1954
1955         if (not product_id) or (product_qty <= 0.0):
1956             result['product_qty'] = 0.0
1957             return {'value': result}
1958
1959         product_obj = self.pool.get('product.product')
1960         uos_coeff = product_obj.read(cr, uid, product_id, ['uos_coeff'])
1961
1962         # Warn if the quantity was decreased
1963         if ids:
1964             for move in self.read(cr, uid, ids, ['product_qty']):
1965                 if product_qty < move['product_qty']:
1966                     warning.update({
1967                         'title': _('Information'),
1968                         'message': _("By changing this quantity here, you accept the "
1969                                 "new quantity as complete: Odoo will not "
1970                                 "automatically generate a back order.")})
1971                 break
1972
1973         if product_uos and product_uom and (product_uom != product_uos):
1974             result['product_uos_qty'] = product_qty * uos_coeff['uos_coeff']
1975         else:
1976             result['product_uos_qty'] = product_qty
1977
1978         return {'value': result, 'warning': warning}
1979
1980     def onchange_uos_quantity(self, cr, uid, ids, product_id, product_uos_qty,
1981                           product_uos, product_uom):
1982         """ On change of product quantity finds UoM and UoS quantities
1983         @param product_id: Product id
1984         @param product_uos_qty: Changed UoS Quantity of product
1985         @param product_uom: Unit of measure of product
1986         @param product_uos: Unit of sale of product
1987         @return: Dictionary of values
1988         """
1989         result = {
1990             'product_uom_qty': 0.00
1991         }
1992
1993         if (not product_id) or (product_uos_qty <= 0.0):
1994             result['product_uos_qty'] = 0.0
1995             return {'value': result}
1996
1997         product_obj = self.pool.get('product.product')
1998         uos_coeff = product_obj.read(cr, uid, product_id, ['uos_coeff'])
1999
2000         # No warning if the quantity was decreased to avoid double warnings:
2001         # The clients should call onchange_quantity too anyway
2002
2003         if product_uos and product_uom and (product_uom != product_uos):
2004             result['product_uom_qty'] = product_uos_qty / uos_coeff['uos_coeff']
2005         else:
2006             result['product_uom_qty'] = product_uos_qty
2007         return {'value': result}
2008
2009     def onchange_product_id(self, cr, uid, ids, prod_id=False, loc_id=False, loc_dest_id=False, partner_id=False):
2010         """ On change of product id, if finds UoM, UoS, quantity and UoS quantity.
2011         @param prod_id: Changed Product id
2012         @param loc_id: Source location id
2013         @param loc_dest_id: Destination location id
2014         @param partner_id: Address id of partner
2015         @return: Dictionary of values
2016         """
2017         if not prod_id:
2018             return {}
2019         user = self.pool.get('res.users').browse(cr, uid, uid)
2020         lang = user and user.lang or False
2021         if partner_id:
2022             addr_rec = self.pool.get('res.partner').browse(cr, uid, partner_id)
2023             if addr_rec:
2024                 lang = addr_rec and addr_rec.lang or False
2025         ctx = {'lang': lang}
2026
2027         product = self.pool.get('product.product').browse(cr, uid, [prod_id], context=ctx)[0]
2028         uos_id = product.uos_id and product.uos_id.id or False
2029         result = {
2030             'name': product.partner_ref,
2031             'product_uom': product.uom_id.id,
2032             'product_uos': uos_id,
2033             'product_uom_qty': 1.00,
2034             'product_uos_qty': self.pool.get('stock.move').onchange_quantity(cr, uid, ids, prod_id, 1.00, product.uom_id.id, uos_id)['value']['product_uos_qty'],
2035         }
2036         if loc_id:
2037             result['location_id'] = loc_id
2038         if loc_dest_id:
2039             result['location_dest_id'] = loc_dest_id
2040         return {'value': result}
2041
2042     @api.cr_uid_ids_context
2043     def _picking_assign(self, cr, uid, move_ids, procurement_group, location_from, location_to, context=None):
2044         """Assign a picking on the given move_ids, which is a list of move supposed to share the same procurement_group, location_from and location_to
2045         (and company). Those attributes are also given as parameters.
2046         """
2047         pick_obj = self.pool.get("stock.picking")
2048         picks = pick_obj.search(cr, uid, [
2049                 ('group_id', '=', procurement_group),
2050                 ('location_id', '=', location_from),
2051                 ('location_dest_id', '=', location_to),
2052                 ('state', 'in', ['draft', 'confirmed', 'waiting'])], context=context)
2053         if picks:
2054             pick = picks[0]
2055         else:
2056             move = self.browse(cr, uid, move_ids, context=context)[0]
2057             values = {
2058                 'origin': move.origin,
2059                 'company_id': move.company_id and move.company_id.id or False,
2060                 'move_type': move.group_id and move.group_id.move_type or 'direct',
2061                 'partner_id': move.partner_id.id or False,
2062                 'picking_type_id': move.picking_type_id and move.picking_type_id.id or False,
2063             }
2064             pick = pick_obj.create(cr, uid, values, context=context)
2065         return self.write(cr, uid, move_ids, {'picking_id': pick}, context=context)
2066
2067     def onchange_date(self, cr, uid, ids, date, date_expected, context=None):
2068         """ On change of Scheduled Date gives a Move date.
2069         @param date_expected: Scheduled Date
2070         @param date: Move Date
2071         @return: Move Date
2072         """
2073         if not date_expected:
2074             date_expected = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
2075         return {'value': {'date': date_expected}}
2076
2077     def attribute_price(self, cr, uid, move, context=None):
2078         """
2079             Attribute price to move, important in inter-company moves or receipts with only one partner
2080         """
2081         if not move.price_unit:
2082             price = move.product_id.standard_price
2083             self.write(cr, uid, [move.id], {'price_unit': price})
2084
2085     def action_confirm(self, cr, uid, ids, context=None):
2086         """ Confirms stock move or put it in waiting if it's linked to another move.
2087         @return: List of ids.
2088         """
2089         if isinstance(ids, (int, long)):
2090             ids = [ids]
2091         states = {
2092             'confirmed': [],
2093             'waiting': []
2094         }
2095         to_assign = {}
2096         for move in self.browse(cr, uid, ids, context=context):
2097             self.attribute_price(cr, uid, move, context=context)
2098             state = 'confirmed'
2099             #if the move is preceeded, then it's waiting (if preceeding move is done, then action_assign has been called already and its state is already available)
2100             if move.move_orig_ids:
2101                 state = 'waiting'
2102             #if the move is split and some of the ancestor was preceeded, then it's waiting as well
2103             elif move.split_from:
2104                 move2 = move.split_from
2105                 while move2 and state != 'waiting':
2106                     if move2.move_orig_ids:
2107                         state = 'waiting'
2108                     move2 = move2.split_from
2109             states[state].append(move.id)
2110
2111             if not move.picking_id and move.picking_type_id:
2112                 key = (move.group_id.id, move.location_id.id, move.location_dest_id.id)
2113                 if key not in to_assign:
2114                     to_assign[key] = []
2115                 to_assign[key].append(move.id)
2116
2117         for move in self.browse(cr, uid, states['confirmed'], context=context):
2118             if move.procure_method == 'make_to_order':
2119                 self._create_procurement(cr, uid, move, context=context)
2120                 states['waiting'].append(move.id)
2121                 states['confirmed'].remove(move.id)
2122
2123         for state, write_ids in states.items():
2124             if len(write_ids):
2125                 self.write(cr, uid, write_ids, {'state': state})
2126         #assign picking in batch for all confirmed move that share the same details
2127         for key, move_ids in to_assign.items():
2128             procurement_group, location_from, location_to = key
2129             self._picking_assign(cr, uid, move_ids, procurement_group, location_from, location_to, context=context)
2130         moves = self.browse(cr, uid, ids, context=context)
2131         self._push_apply(cr, uid, moves, context=context)
2132         return ids
2133
2134     def force_assign(self, cr, uid, ids, context=None):
2135         """ Changes the state to assigned.
2136         @return: True
2137         """
2138         return self.write(cr, uid, ids, {'state': 'assigned'}, context=context)
2139
2140     def check_tracking_product(self, cr, uid, product, lot_id, location, location_dest, context=None):
2141         check = False
2142         if product.track_all and not location_dest.usage == 'inventory':
2143             check = True
2144         elif product.track_incoming and location.usage in ('supplier', 'transit', 'inventory') and location_dest.usage == 'internal':
2145             check = True
2146         elif product.track_outgoing and location_dest.usage in ('customer', 'transit') and location.usage == 'internal':
2147             check = True
2148         if check and not lot_id:
2149             raise osv.except_osv(_('Warning!'), _('You must assign a serial number for the product %s') % (product.name))
2150
2151
2152     def check_tracking(self, cr, uid, move, lot_id, context=None):
2153         """ Checks if serial number is assigned to stock move or not and raise an error if it had to.
2154         """
2155         self.check_tracking_product(cr, uid, move.product_id, lot_id, move.location_id, move.location_dest_id, context=context)
2156         
2157
2158     def action_assign(self, cr, uid, ids, context=None):
2159         """ Checks the product type and accordingly writes the state.
2160         """
2161         context = context or {}
2162         quant_obj = self.pool.get("stock.quant")
2163         to_assign_moves = []
2164         main_domain = {}
2165         todo_moves = []
2166         operations = set()
2167         for move in self.browse(cr, uid, ids, context=context):
2168             if move.state not in ('confirmed', 'waiting', 'assigned'):
2169                 continue
2170             if move.location_id.usage in ('supplier', 'inventory', 'production'):
2171                 to_assign_moves.append(move.id)
2172                 #in case the move is returned, we want to try to find quants before forcing the assignment
2173                 if not move.origin_returned_move_id:
2174                     continue
2175             if move.product_id.type == 'consu':
2176                 to_assign_moves.append(move.id)
2177                 continue
2178             else:
2179                 todo_moves.append(move)
2180
2181                 #we always keep the quants already assigned and try to find the remaining quantity on quants not assigned only
2182                 main_domain[move.id] = [('reservation_id', '=', False), ('qty', '>', 0)]
2183
2184                 #if the move is preceeded, restrict the choice of quants in the ones moved previously in original move
2185                 ancestors = self.find_move_ancestors(cr, uid, move, context=context)
2186                 if move.state == 'waiting' and not ancestors:
2187                     #if the waiting move hasn't yet any ancestor (PO/MO not confirmed yet), don't find any quant available in stock
2188                     main_domain[move.id] += [('id', '=', False)]
2189                 elif ancestors:
2190                     main_domain[move.id] += [('history_ids', 'in', ancestors)]
2191
2192                 #if the move is returned from another, restrict the choice of quants to the ones that follow the returned move
2193                 if move.origin_returned_move_id:
2194                     main_domain[move.id] += [('history_ids', 'in', move.origin_returned_move_id.id)]
2195                 for link in move.linked_move_operation_ids:
2196                     operations.add(link.operation_id)
2197         # Check all ops and sort them: we want to process first the packages, then operations with lot then the rest
2198         operations = list(operations)
2199         operations.sort(key=lambda x: ((x.package_id and not x.product_id) and -4 or 0) + (x.package_id and -2 or 0) + (x.lot_id and -1 or 0))
2200         for ops in operations:
2201             #first try to find quants based on specific domains given by linked operations
2202             for record in ops.linked_move_operation_ids:
2203                 move = record.move_id
2204                 if move.id in main_domain:
2205                     domain = main_domain[move.id] + self.pool.get('stock.move.operation.link').get_specific_domain(cr, uid, record, context=context)
2206                     qty = record.qty
2207                     if qty:
2208                         quants = quant_obj.quants_get_prefered_domain(cr, uid, ops.location_id, move.product_id, qty, domain=domain, prefered_domain_list=[], restrict_lot_id=move.restrict_lot_id.id, restrict_partner_id=move.restrict_partner_id.id, context=context)
2209                         quant_obj.quants_reserve(cr, uid, quants, move, record, context=context)
2210         for move in todo_moves:
2211             if move.linked_move_operation_ids:
2212                 continue
2213             move.refresh()
2214             #then if the move isn't totally assigned, try to find quants without any specific domain
2215             if move.state != 'assigned':
2216                 qty_already_assigned = move.reserved_availability
2217                 qty = move.product_qty - qty_already_assigned
2218                 quants = quant_obj.quants_get_prefered_domain(cr, uid, move.location_id, move.product_id, qty, domain=main_domain[move.id], prefered_domain_list=[], restrict_lot_id=move.restrict_lot_id.id, restrict_partner_id=move.restrict_partner_id.id, context=context)
2219                 quant_obj.quants_reserve(cr, uid, quants, move, context=context)
2220
2221         #force assignation of consumable products and incoming from supplier/inventory/production
2222         if to_assign_moves:
2223             self.force_assign(cr, uid, to_assign_moves, context=context)
2224
2225     def action_cancel(self, cr, uid, ids, context=None):
2226         """ Cancels the moves and if all moves are cancelled it cancels the picking.
2227         @return: True
2228         """
2229         procurement_obj = self.pool.get('procurement.order')
2230         context = context or {}
2231         procs_to_check = []
2232         for move in self.browse(cr, uid, ids, context=context):
2233             if move.state == 'done':
2234                 raise osv.except_osv(_('Operation Forbidden!'),
2235                         _('You cannot cancel a stock move that has been set to \'Done\'.'))
2236             if move.reserved_quant_ids:
2237                 self.pool.get("stock.quant").quants_unreserve(cr, uid, move, context=context)
2238             if context.get('cancel_procurement'):
2239                 if move.propagate:
2240                     procurement_ids = procurement_obj.search(cr, uid, [('move_dest_id', '=', move.id)], context=context)
2241                     procurement_obj.cancel(cr, uid, procurement_ids, context=context)
2242             else:
2243                 if move.move_dest_id:
2244                     if move.propagate:
2245                         self.action_cancel(cr, uid, [move.move_dest_id.id], context=context)
2246                     elif move.move_dest_id.state == 'waiting':
2247                         #If waiting, the chain will be broken and we are not sure if we can still wait for it (=> could take from stock instead)
2248                         self.write(cr, uid, [move.move_dest_id.id], {'state': 'confirmed'}, context=context)
2249                 if move.procurement_id:
2250                     # Does the same as procurement check, only eliminating a refresh
2251                     procs_to_check.append(move.procurement_id.id)
2252                     
2253         res = self.write(cr, uid, ids, {'state': 'cancel', 'move_dest_id': False}, context=context)
2254         if procs_to_check:
2255             procurement_obj.check(cr, uid, procs_to_check, context=context)
2256         return res
2257
2258     def _check_package_from_moves(self, cr, uid, ids, context=None):
2259         pack_obj = self.pool.get("stock.quant.package")
2260         packs = set()
2261         for move in self.browse(cr, uid, ids, context=context):
2262             packs |= set([q.package_id for q in move.quant_ids if q.package_id and q.qty > 0])
2263         return pack_obj._check_location_constraint(cr, uid, list(packs), context=context)
2264
2265     def find_move_ancestors(self, cr, uid, move, context=None):
2266         '''Find the first level ancestors of given move '''
2267         ancestors = []
2268         move2 = move
2269         while move2:
2270             ancestors += [x.id for x in move2.move_orig_ids]
2271             #loop on the split_from to find the ancestor of split moves only if the move has not direct ancestor (priority goes to them)
2272             move2 = not move2.move_orig_ids and move2.split_from or False
2273         return ancestors
2274
2275     @api.cr_uid_ids_context
2276     def recalculate_move_state(self, cr, uid, move_ids, context=None):
2277         '''Recompute the state of moves given because their reserved quants were used to fulfill another operation'''
2278         for move in self.browse(cr, uid, move_ids, context=context):
2279             vals = {}
2280             reserved_quant_ids = move.reserved_quant_ids
2281             if len(reserved_quant_ids) > 0 and not move.partially_available:
2282                 vals['partially_available'] = True
2283             if len(reserved_quant_ids) == 0 and move.partially_available:
2284                 vals['partially_available'] = False
2285             if move.state == 'assigned':
2286                 if self.find_move_ancestors(cr, uid, move, context=context):
2287                     vals['state'] = 'waiting'
2288                 else:
2289                     vals['state'] = 'confirmed'
2290             if vals:
2291                 self.write(cr, uid, [move.id], vals, context=context)
2292
2293     def action_done(self, cr, uid, ids, context=None):
2294         """ Process completely the moves given as ids and if all moves are done, it will finish the picking.
2295         """
2296         context = context or {}
2297         picking_obj = self.pool.get("stock.picking")
2298         quant_obj = self.pool.get("stock.quant")
2299         todo = [move.id for move in self.browse(cr, uid, ids, context=context) if move.state == "draft"]
2300         if todo:
2301             ids = self.action_confirm(cr, uid, todo, context=context)
2302         pickings = set()
2303         procurement_ids = []
2304         #Search operations that are linked to the moves
2305         operations = set()
2306         move_qty = {}
2307         for move in self.browse(cr, uid, ids, context=context):
2308             move_qty[move.id] = move.product_qty
2309             for link in move.linked_move_operation_ids:
2310                 operations.add(link.operation_id)
2311
2312         #Sort operations according to entire packages first, then package + lot, package only, lot only
2313         operations = list(operations)
2314         operations.sort(key=lambda x: ((x.package_id and not x.product_id) and -4 or 0) + (x.package_id and -2 or 0) + (x.lot_id and -1 or 0))
2315
2316         for ops in operations:
2317             if ops.picking_id:
2318                 pickings.add(ops.picking_id.id)
2319             main_domain = [('qty', '>', 0)]
2320             for record in ops.linked_move_operation_ids:
2321                 move = record.move_id
2322                 self.check_tracking(cr, uid, move, not ops.product_id and ops.package_id.id or ops.lot_id.id, context=context)
2323                 prefered_domain = [('reservation_id', '=', move.id)]
2324                 fallback_domain = [('reservation_id', '=', False)]
2325                 fallback_domain2 = ['&', ('reservation_id', '!=', move.id), ('reservation_id', '!=', False)]
2326                 prefered_domain_list = [prefered_domain] + [fallback_domain] + [fallback_domain2]
2327                 dom = main_domain + self.pool.get('stock.move.operation.link').get_specific_domain(cr, uid, record, context=context)
2328                 quants = quant_obj.quants_get_prefered_domain(cr, uid, ops.location_id, move.product_id, record.qty, domain=dom, prefered_domain_list=prefered_domain_list,
2329                                                           restrict_lot_id=move.restrict_lot_id.id, restrict_partner_id=move.restrict_partner_id.id, context=context)
2330                 if ops.result_package_id.id:
2331                     #if a result package is given, all quants go there
2332                     quant_dest_package_id = ops.result_package_id.id
2333                 elif ops.product_id and ops.package_id:
2334                     #if a package and a product is given, we will remove quants from the pack.
2335                     quant_dest_package_id = False
2336                 else:
2337                     #otherwise we keep the current pack of the quant, which may mean None
2338                     quant_dest_package_id = ops.package_id.id
2339                 quant_obj.quants_move(cr, uid, quants, move, ops.location_dest_id, location_from=ops.location_id, lot_id=ops.lot_id.id, owner_id=ops.owner_id.id, src_package_id=ops.package_id.id, dest_package_id=quant_dest_package_id, context=context)
2340                 # Handle pack in pack
2341                 if not ops.product_id and ops.package_id and ops.result_package_id.id != ops.package_id.parent_id.id:
2342                     self.pool.get('stock.quant.package').write(cr, SUPERUSER_ID, [ops.package_id.id], {'parent_id': ops.result_package_id.id}, context=context)
2343                 if not move_qty.get(move.id):
2344                     raise osv.except_osv(_("Error"), _("The roundings of your Unit of Measures %s on the move vs. %s on the product don't allow to do these operations or you are not transferring the picking at once. ") % (move.product_uom.name, move.product_id.uom_id.name))
2345                 move_qty[move.id] -= record.qty
2346         #Check for remaining qtys and unreserve/check move_dest_id in
2347         move_dest_ids = set()
2348         for move in self.browse(cr, uid, ids, context=context):
2349             move_qty_cmp = float_compare(move_qty[move.id], 0, precision_rounding=move.product_id.uom_id.rounding)
2350             if move_qty_cmp > 0:  # (=In case no pack operations in picking)
2351                 main_domain = [('qty', '>', 0)]
2352                 prefered_domain = [('reservation_id', '=', move.id)]
2353                 fallback_domain = [('reservation_id', '=', False)]
2354                 fallback_domain2 = ['&', ('reservation_id', '!=', move.id), ('reservation_id', '!=', False)]
2355                 prefered_domain_list = [prefered_domain] + [fallback_domain] + [fallback_domain2]
2356                 self.check_tracking(cr, uid, move, move.restrict_lot_id.id, context=context)
2357                 qty = move_qty[move.id]
2358                 quants = quant_obj.quants_get_prefered_domain(cr, uid, move.location_id, move.product_id, qty, domain=main_domain, prefered_domain_list=prefered_domain_list, restrict_lot_id=move.restrict_lot_id.id, restrict_partner_id=move.restrict_partner_id.id, context=context)
2359                 quant_obj.quants_move(cr, uid, quants, move, move.location_dest_id, lot_id=move.restrict_lot_id.id, owner_id=move.restrict_partner_id.id, context=context)
2360
2361             # If the move has a destination, add it to the list to reserve
2362             if move.move_dest_id and move.move_dest_id.state in ('waiting', 'confirmed'):
2363                 move_dest_ids.add(move.move_dest_id.id)
2364
2365             if move.procurement_id:
2366                 procurement_ids.append(move.procurement_id.id)
2367
2368             #unreserve the quants and make them available for other operations/moves
2369             quant_obj.quants_unreserve(cr, uid, move, context=context)
2370         # Check the packages have been placed in the correct locations
2371         self._check_package_from_moves(cr, uid, ids, context=context)
2372         #set the move as done
2373         self.write(cr, uid, ids, {'state': 'done', 'date': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context)
2374         self.pool.get('procurement.order').check(cr, uid, procurement_ids, context=context)
2375         #assign destination moves
2376         if move_dest_ids:
2377             self.action_assign(cr, uid, list(move_dest_ids), context=context)
2378         #check picking state to set the date_done is needed
2379         done_picking = []
2380         for picking in picking_obj.browse(cr, uid, list(pickings), context=context):
2381             if picking.state == 'done' and not picking.date_done:
2382                 done_picking.append(picking.id)
2383         if done_picking:
2384             picking_obj.write(cr, uid, done_picking, {'date_done': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context)
2385         return True
2386
2387     def unlink(self, cr, uid, ids, context=None):
2388         context = context or {}
2389         for move in self.browse(cr, uid, ids, context=context):
2390             if move.state not in ('draft', 'cancel'):
2391                 raise osv.except_osv(_('User Error!'), _('You can only delete draft moves.'))
2392         return super(stock_move, self).unlink(cr, uid, ids, context=context)
2393
2394     def action_scrap(self, cr, uid, ids, quantity, location_id, restrict_lot_id=False, restrict_partner_id=False, context=None):
2395         """ Move the scrap/damaged product into scrap location
2396         @param cr: the database cursor
2397         @param uid: the user id
2398         @param ids: ids of stock move object to be scrapped
2399         @param quantity : specify scrap qty
2400         @param location_id : specify scrap location
2401         @param context: context arguments
2402         @return: Scraped lines
2403         """
2404         #quantity should be given in MOVE UOM
2405         if quantity <= 0:
2406             raise osv.except_osv(_('Warning!'), _('Please provide a positive quantity to scrap.'))
2407         res = []
2408         for move in self.browse(cr, uid, ids, context=context):
2409             source_location = move.location_id
2410             if move.state == 'done':
2411                 source_location = move.location_dest_id
2412             #Previously used to prevent scraping from virtual location but not necessary anymore
2413             #if source_location.usage != 'internal':
2414                 #restrict to scrap from a virtual location because it's meaningless and it may introduce errors in stock ('creating' new products from nowhere)
2415                 #raise osv.except_osv(_('Error!'), _('Forbidden operation: it is not allowed to scrap products from a virtual location.'))
2416             move_qty = move.product_qty
2417             uos_qty = quantity / move_qty * move.product_uos_qty
2418             default_val = {
2419                 'location_id': source_location.id,
2420                 'product_uom_qty': quantity,
2421                 'product_uos_qty': uos_qty,
2422                 'state': move.state,
2423                 'scrapped': True,
2424                 'location_dest_id': location_id,
2425                 'restrict_lot_id': restrict_lot_id,
2426                 'restrict_partner_id': restrict_partner_id,
2427             }
2428             new_move = self.copy(cr, uid, move.id, default_val)
2429
2430             res += [new_move]
2431             product_obj = self.pool.get('product.product')
2432             for product in product_obj.browse(cr, uid, [move.product_id.id], context=context):
2433                 if move.picking_id:
2434                     uom = product.uom_id.name if product.uom_id else ''
2435                     message = _("%s %s %s has been <b>moved to</b> scrap.") % (quantity, uom, product.name)
2436                     move.picking_id.message_post(body=message)
2437
2438         self.action_done(cr, uid, res, context=context)
2439         return res
2440
2441     def split(self, cr, uid, move, qty, restrict_lot_id=False, restrict_partner_id=False, context=None):
2442         """ Splits qty from move move into a new move
2443         :param move: browse record
2444         :param qty: float. quantity to split (given in product UoM)
2445         :param restrict_lot_id: optional production lot that can be given in order to force the new move to restrict its choice of quants to this lot.
2446         :param restrict_partner_id: optional partner that can be given in order to force the new move to restrict its choice of quants to the ones belonging to this partner.
2447         :param context: dictionay. can contains the special key 'source_location_id' in order to force the source location when copying the move
2448
2449         returns the ID of the backorder move created
2450         """
2451         if move.state in ('done', 'cancel'):
2452             raise osv.except_osv(_('Error'), _('You cannot split a move done'))
2453         if move.state == 'draft':
2454             #we restrict the split of a draft move because if not confirmed yet, it may be replaced by several other moves in
2455             #case of phantom bom (with mrp module). And we don't want to deal with this complexity by copying the product that will explode.
2456             raise osv.except_osv(_('Error'), _('You cannot split a draft move. It needs to be confirmed first.'))
2457
2458         if move.product_qty <= qty or qty == 0:
2459             return move.id
2460
2461         uom_obj = self.pool.get('product.uom')
2462         context = context or {}
2463
2464         uom_qty = uom_obj._compute_qty_obj(cr, uid, move.product_id.uom_id, qty, move.product_uom)
2465         uos_qty = uom_qty * move.product_uos_qty / move.product_uom_qty
2466
2467         defaults = {
2468             'product_uom_qty': uom_qty,
2469             'product_uos_qty': uos_qty,
2470             'procure_method': 'make_to_stock',
2471             'restrict_lot_id': restrict_lot_id,
2472             'restrict_partner_id': restrict_partner_id,
2473             'split_from': move.id,
2474             'procurement_id': move.procurement_id.id,
2475             'move_dest_id': move.move_dest_id.id,
2476         }
2477         if context.get('source_location_id'):
2478             defaults['location_id'] = context['source_location_id']
2479         new_move = self.copy(cr, uid, move.id, defaults)
2480
2481         ctx = context.copy()
2482         ctx['do_not_propagate'] = True
2483         self.write(cr, uid, [move.id], {
2484             'product_uom_qty': move.product_uom_qty - uom_qty,
2485             'product_uos_qty': move.product_uos_qty - uos_qty,
2486         }, context=ctx)
2487
2488         if move.move_dest_id and move.propagate and move.move_dest_id.state not in ('done', 'cancel'):
2489             new_move_prop = self.split(cr, uid, move.move_dest_id, qty, context=context)
2490             self.write(cr, uid, [new_move], {'move_dest_id': new_move_prop}, context=context)
2491         #returning the first element of list returned by action_confirm is ok because we checked it wouldn't be exploded (and
2492         #thus the result of action_confirm should always be a list of 1 element length)
2493         return self.action_confirm(cr, uid, [new_move], context=context)[0]
2494
2495
2496     def get_code_from_locs(self, cr, uid, move, location_id=False, location_dest_id=False, context=None):
2497         """
2498         Returns the code the picking type should have.  This can easily be used
2499         to check if a move is internal or not
2500         move, location_id and location_dest_id are browse records
2501         """
2502         code = 'internal'
2503         src_loc = location_id or move.location_id
2504         dest_loc = location_dest_id or move.location_dest_id
2505         if src_loc.usage == 'internal' and dest_loc.usage != 'internal':
2506             code = 'outgoing'
2507         if src_loc.usage != 'internal' and dest_loc.usage == 'internal':
2508             code = 'incoming'
2509         return code
2510
2511
2512 class stock_inventory(osv.osv):
2513     _name = "stock.inventory"
2514     _description = "Inventory"
2515
2516     def _get_move_ids_exist(self, cr, uid, ids, field_name, arg, context=None):
2517         res = {}
2518         for inv in self.browse(cr, uid, ids, context=context):
2519             res[inv.id] = False
2520             if inv.move_ids:
2521                 res[inv.id] = True
2522         return res
2523
2524     def _get_available_filters(self, cr, uid, context=None):
2525         """
2526            This function will return the list of filter allowed according to the options checked
2527            in 'Settings\Warehouse'.
2528
2529            :rtype: list of tuple
2530         """
2531         #default available choices
2532         res_filter = [('none', _('All products')), ('product', _('One product only'))]
2533         settings_obj = self.pool.get('stock.config.settings')
2534         config_ids = settings_obj.search(cr, uid, [], limit=1, order='id DESC', context=context)
2535         #If we don't have updated config until now, all fields are by default false and so should be not dipslayed
2536         if not config_ids:
2537             return res_filter
2538
2539         stock_settings = settings_obj.browse(cr, uid, config_ids[0], context=context)
2540         if stock_settings.group_stock_tracking_owner:
2541             res_filter.append(('owner', _('One owner only')))
2542             res_filter.append(('product_owner', _('One product for a specific owner')))
2543         if stock_settings.group_stock_tracking_lot:
2544             res_filter.append(('lot', _('One Lot/Serial Number')))
2545         if stock_settings.group_stock_packaging:
2546             res_filter.append(('pack', _('A Pack')))
2547         return res_filter
2548
2549     def _get_total_qty(self, cr, uid, ids, field_name, args, context=None):
2550         res = {}
2551         for inv in self.browse(cr, uid, ids, context=context):
2552             res[inv.id] = sum([x.product_qty for x in inv.line_ids])
2553         return res
2554
2555     INVENTORY_STATE_SELECTION = [
2556         ('draft', 'Draft'),
2557         ('cancel', 'Cancelled'),
2558         ('confirm', 'In Progress'),
2559         ('done', 'Validated'),
2560     ]
2561
2562     _columns = {
2563         'name': fields.char('Inventory Reference', required=True, readonly=True, states={'draft': [('readonly', False)]}, help="Inventory Name."),
2564         'date': fields.datetime('Inventory Date', required=True, readonly=True, help="The date that will be used for the stock level check of the products and the validation of the stock move related to this inventory."),
2565         'line_ids': fields.one2many('stock.inventory.line', 'inventory_id', 'Inventories', readonly=False, states={'done': [('readonly', True)]}, help="Inventory Lines.", copy=True),
2566         'move_ids': fields.one2many('stock.move', 'inventory_id', 'Created Moves', help="Inventory Moves.", states={'done': [('readonly', True)]}),
2567         'state': fields.selection(INVENTORY_STATE_SELECTION, 'Status', readonly=True, select=True, copy=False),
2568         'company_id': fields.many2one('res.company', 'Company', required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
2569         'location_id': fields.many2one('stock.location', 'Inventoried Location', required=True, readonly=True, states={'draft': [('readonly', False)]}),
2570         'product_id': fields.many2one('product.product', 'Inventoried Product', readonly=True, states={'draft': [('readonly', False)]}, help="Specify Product to focus your inventory on a particular Product."),
2571         'package_id': fields.many2one('stock.quant.package', 'Inventoried Pack', readonly=True, states={'draft': [('readonly', False)]}, help="Specify Pack to focus your inventory on a particular Pack."),
2572         'partner_id': fields.many2one('res.partner', 'Inventoried Owner', readonly=True, states={'draft': [('readonly', False)]}, help="Specify Owner to focus your inventory on a particular Owner."),
2573         'lot_id': fields.many2one('stock.production.lot', 'Inventoried Lot/Serial Number', readonly=True, states={'draft': [('readonly', False)]}, help="Specify Lot/Serial Number to focus your inventory on a particular Lot/Serial Number.", copy=False),
2574         'move_ids_exist': fields.function(_get_move_ids_exist, type='boolean', string=' Stock Move Exists?', help='technical field for attrs in view'),
2575         'filter': fields.selection(_get_available_filters, 'Selection Filter', required=True),
2576         'total_qty': fields.function(_get_total_qty, type="float"),
2577     }
2578
2579     def _default_stock_location(self, cr, uid, context=None):
2580         try:
2581             warehouse = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'warehouse0')
2582             return warehouse.lot_stock_id.id
2583         except:
2584             return False
2585
2586     _defaults = {
2587         'date': fields.datetime.now,
2588         'state': 'draft',
2589         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.inventory', context=c),
2590         'location_id': _default_stock_location,
2591         'filter': 'none',
2592     }
2593
2594     def reset_real_qty(self, cr, uid, ids, context=None):
2595         inventory = self.browse(cr, uid, ids[0], context=context)
2596         line_ids = [line.id for line in inventory.line_ids]
2597         self.pool.get('stock.inventory.line').write(cr, uid, line_ids, {'product_qty': 0})
2598         return True
2599
2600     def _inventory_line_hook(self, cr, uid, inventory_line, move_vals):
2601         """ Creates a stock move from an inventory line
2602         @param inventory_line:
2603         @param move_vals:
2604         @return:
2605         """
2606         return self.pool.get('stock.move').create(cr, uid, move_vals)
2607
2608     def action_done(self, cr, uid, ids, context=None):
2609         """ Finish the inventory
2610         @return: True
2611         """
2612         for inv in self.browse(cr, uid, ids, context=context):
2613             for inventory_line in inv.line_ids:
2614                 if inventory_line.product_qty < 0 and inventory_line.product_qty != inventory_line.theoretical_qty:
2615                     raise osv.except_osv(_('Warning'), _('You cannot set a negative product quantity in an inventory line:\n\t%s - qty: %s' % (inventory_line.product_id.name, inventory_line.product_qty)))
2616             self.action_check(cr, uid, [inv.id], context=context)
2617             inv.refresh()
2618             self.write(cr, uid, [inv.id], {'state': 'done'}, context=context)
2619             self.post_inventory(cr, uid, inv, context=context)
2620         return True
2621
2622     def post_inventory(self, cr, uid, inv, context=None):
2623         #The inventory is posted as a single step which means quants cannot be moved from an internal location to another using an inventory
2624         #as they will be moved to inventory loss, and other quants will be created to the encoded quant location. This is a normal behavior
2625         #as quants cannot be reuse from inventory location (users can still manually move the products before/after the inventory if they want).
2626         move_obj = self.pool.get('stock.move')
2627         move_obj.action_done(cr, uid, [x.id for x in inv.move_ids], context=context)
2628
2629     def _create_stock_move(self, cr, uid, inventory, todo_line, context=None):
2630         stock_move_obj = self.pool.get('stock.move')
2631         product_obj = self.pool.get('product.product')
2632         inventory_location_id = product_obj.browse(cr, uid, todo_line['product_id'], context=context).property_stock_inventory.id
2633         vals = {
2634             'name': _('INV:') + (inventory.name or ''),
2635             'product_id': todo_line['product_id'],
2636             'product_uom': todo_line['product_uom_id'],
2637             'date': inventory.date,
2638             'company_id': inventory.company_id.id,
2639             'inventory_id': inventory.id,
2640             'state': 'assigned',
2641             'restrict_lot_id': todo_line.get('prod_lot_id'),
2642             'restrict_partner_id': todo_line.get('partner_id'),
2643          }
2644
2645         if todo_line['product_qty'] < 0:
2646             #found more than expected
2647             vals['location_id'] = inventory_location_id
2648             vals['location_dest_id'] = todo_line['location_id']
2649             vals['product_uom_qty'] = -todo_line['product_qty']
2650         else:
2651             #found less than expected
2652             vals['location_id'] = todo_line['location_id']
2653             vals['location_dest_id'] = inventory_location_id
2654             vals['product_uom_qty'] = todo_line['product_qty']
2655         return stock_move_obj.create(cr, uid, vals, context=context)
2656
2657     def action_check(self, cr, uid, ids, context=None):
2658         """ Checks the inventory and computes the stock move to do
2659         @return: True
2660         """
2661         inventory_line_obj = self.pool.get('stock.inventory.line')
2662         stock_move_obj = self.pool.get('stock.move')
2663         for inventory in self.browse(cr, uid, ids, context=context):
2664             #first remove the existing stock moves linked to this inventory
2665             move_ids = [move.id for move in inventory.move_ids]
2666             stock_move_obj.unlink(cr, uid, move_ids, context=context)
2667             for line in inventory.line_ids:
2668                 #compare the checked quantities on inventory lines to the theorical one
2669                 inventory_line_obj._resolve_inventory_line(cr, uid, line, context=context)
2670
2671     def action_cancel_draft(self, cr, uid, ids, context=None):
2672         """ Cancels the stock move and change inventory state to draft.
2673         @return: True
2674         """
2675         for inv in self.browse(cr, uid, ids, context=context):
2676             self.pool.get('stock.move').action_cancel(cr, uid, [x.id for x in inv.move_ids], context=context)
2677             self.write(cr, uid, [inv.id], {'state': 'draft'}, context=context)
2678         return True
2679
2680     def action_cancel_inventory(self, cr, uid, ids, context=None):
2681         self.action_cancel_draft(cr, uid, ids, context=context)
2682
2683     def prepare_inventory(self, cr, uid, ids, context=None):
2684         inventory_line_obj = self.pool.get('stock.inventory.line')
2685         for inventory in self.browse(cr, uid, ids, context=context):
2686             # If there are inventory lines already (e.g. from import), respect those and set their theoretical qty
2687             line_ids = [line.id for line in inventory.line_ids]
2688             if not line_ids:
2689                 #compute the inventory lines and create them
2690                 vals = self._get_inventory_lines(cr, uid, inventory, context=context)
2691                 for product_line in vals:
2692                     inventory_line_obj.create(cr, uid, product_line, context=context)
2693             else:
2694                 # On import calculate theoretical quantity
2695                 quant_obj = self.pool.get("stock.quant")
2696                 for line in inventory.line_ids:
2697                     dom = [('company_id', '=', line.company_id.id), ('location_id', 'child_of', line.location_id.id), ('lot_id', '=', line.prod_lot_id.id),
2698                         ('product_id','=', line.product_id.id), ('owner_id', '=', line.partner_id.id)]
2699                     if line.package_id:
2700                         dom += [('package_id', '=', line.package_id.id)]
2701                     quants = quant_obj.search(cr, uid, dom, context=context)
2702                     tot_qty = 0
2703                     for quant in quant_obj.browse(cr, uid, quants, context=context):
2704                         tot_qty += quant.qty
2705                     inventory_line_obj.write(cr, uid, [line.id], {'theoretical_qty': tot_qty}, context=context)
2706
2707         return self.write(cr, uid, ids, {'state': 'confirm', 'date': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)})
2708
2709     def _get_inventory_lines(self, cr, uid, inventory, context=None):
2710         location_obj = self.pool.get('stock.location')
2711         product_obj = self.pool.get('product.product')
2712         location_ids = location_obj.search(cr, uid, [('id', 'child_of', [inventory.location_id.id])], context=context)
2713         domain = ' location_id in %s'
2714         args = (tuple(location_ids),)
2715         if inventory.partner_id:
2716             domain += ' and owner_id = %s'
2717             args += (inventory.partner_id.id,)
2718         if inventory.lot_id:
2719             domain += ' and lot_id = %s'
2720             args += (inventory.lot_id.id,)
2721         if inventory.product_id:
2722             domain += ' and product_id = %s'
2723             args += (inventory.product_id.id,)
2724         if inventory.package_id:
2725             domain += ' and package_id = %s'
2726             args += (inventory.package_id.id,)
2727
2728         cr.execute('''
2729            SELECT product_id, sum(qty) as product_qty, location_id, lot_id as prod_lot_id, package_id, owner_id as partner_id
2730            FROM stock_quant WHERE''' + domain + '''
2731            GROUP BY product_id, location_id, lot_id, package_id, partner_id
2732         ''', args)
2733         vals = []
2734         for product_line in cr.dictfetchall():
2735             #replace the None the dictionary by False, because falsy values are tested later on
2736             for key, value in product_line.items():
2737                 if not value:
2738                     product_line[key] = False
2739             product_line['inventory_id'] = inventory.id
2740             product_line['theoretical_qty'] = product_line['product_qty']
2741             if product_line['product_id']:
2742                 product = product_obj.browse(cr, uid, product_line['product_id'], context=context)
2743                 product_line['product_uom_id'] = product.uom_id.id
2744             vals.append(product_line)
2745         return vals
2746
2747
2748 class stock_inventory_line(osv.osv):
2749     _name = "stock.inventory.line"
2750     _description = "Inventory Line"
2751     _order = "inventory_id, location_name, product_code, product_name, prodlot_name"
2752
2753     def _get_product_name_change(self, cr, uid, ids, context=None):
2754         return self.pool.get('stock.inventory.line').search(cr, uid, [('product_id', 'in', ids)], context=context)
2755
2756     def _get_location_change(self, cr, uid, ids, context=None):
2757         return self.pool.get('stock.inventory.line').search(cr, uid, [('location_id', 'in', ids)], context=context)
2758
2759     def _get_prodlot_change(self, cr, uid, ids, context=None):
2760         return self.pool.get('stock.inventory.line').search(cr, uid, [('prod_lot_id', 'in', ids)], context=context)
2761
2762     _columns = {
2763         'inventory_id': fields.many2one('stock.inventory', 'Inventory', ondelete='cascade', select=True),
2764         'location_id': fields.many2one('stock.location', 'Location', required=True, select=True),
2765         'product_id': fields.many2one('product.product', 'Product', required=True, select=True),
2766         'package_id': fields.many2one('stock.quant.package', 'Pack', select=True),
2767         'product_uom_id': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
2768         'product_qty': fields.float('Checked Quantity', digits_compute=dp.get_precision('Product Unit of Measure')),
2769         'company_id': fields.related('inventory_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, select=True, readonly=True),
2770         'prod_lot_id': fields.many2one('stock.production.lot', 'Serial Number', domain="[('product_id','=',product_id)]"),
2771         'state': fields.related('inventory_id', 'state', type='char', string='Status', readonly=True),
2772         'theoretical_qty': fields.float('Theoretical Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), readonly=True),
2773         'partner_id': fields.many2one('res.partner', 'Owner'),
2774         'product_name': fields.related('product_id', 'name', type='char', string='Product Name', store={
2775                                                                                             'product.product': (_get_product_name_change, ['name', 'default_code'], 20),
2776                                                                                             'stock.inventory.line': (lambda self, cr, uid, ids, c={}: ids, ['product_id'], 20),}),
2777         'product_code': fields.related('product_id', 'default_code', type='char', string='Product Code', store={
2778                                                                                             'product.product': (_get_product_name_change, ['name', 'default_code'], 20),
2779                                                                                             'stock.inventory.line': (lambda self, cr, uid, ids, c={}: ids, ['product_id'], 20),}),
2780         'location_name': fields.related('location_id', 'complete_name', type='char', string='Location Name', store={
2781                                                                                             'stock.location': (_get_location_change, ['name', 'location_id', 'active'], 20),
2782                                                                                             'stock.inventory.line': (lambda self, cr, uid, ids, c={}: ids, ['location_id'], 20),}),
2783         'prodlot_name': fields.related('prod_lot_id', 'name', type='char', string='Serial Number Name', store={
2784                                                                                             'stock.production.lot': (_get_prodlot_change, ['name'], 20),
2785                                                                                             'stock.inventory.line': (lambda self, cr, uid, ids, c={}: ids, ['prod_lot_id'], 20),}),
2786     }
2787
2788     _defaults = {
2789         'product_qty': 1,
2790     }
2791
2792     def _resolve_inventory_line(self, cr, uid, inventory_line, context=None):
2793         stock_move_obj = self.pool.get('stock.move')
2794         diff = inventory_line.theoretical_qty - inventory_line.product_qty
2795         if not diff:
2796             return
2797         #each theorical_lines where difference between theoretical and checked quantities is not 0 is a line for which we need to create a stock move
2798         vals = {
2799             'name': _('INV:') + (inventory_line.inventory_id.name or ''),
2800             'product_id': inventory_line.product_id.id,
2801             'product_uom': inventory_line.product_uom_id.id,
2802             'date': inventory_line.inventory_id.date,
2803             'company_id': inventory_line.inventory_id.company_id.id,
2804             'inventory_id': inventory_line.inventory_id.id,
2805             'state': 'confirmed',
2806             'restrict_lot_id': inventory_line.prod_lot_id.id,
2807             'restrict_partner_id': inventory_line.partner_id.id,
2808          }
2809         inventory_location_id = inventory_line.product_id.property_stock_inventory.id
2810         if diff < 0:
2811             #found more than expected
2812             vals['location_id'] = inventory_location_id
2813             vals['location_dest_id'] = inventory_line.location_id.id
2814             vals['product_uom_qty'] = -diff
2815         else:
2816             #found less than expected
2817             vals['location_id'] = inventory_line.location_id.id
2818             vals['location_dest_id'] = inventory_location_id
2819             vals['product_uom_qty'] = diff
2820         return stock_move_obj.create(cr, uid, vals, context=context)
2821
2822     def restrict_change(self, cr, uid, ids, theoretical_qty, context=None):
2823         if ids and theoretical_qty:
2824             #if the user try to modify a line prepared by openerp, reject the change and display an error message explaining how he should do
2825             old_value = self.browse(cr, uid, ids[0], context=context)
2826             return {
2827                 'value': {
2828                     'product_id': old_value.product_id.id,
2829                     'product_uom_id': old_value.product_uom_id.id,
2830                     'location_id': old_value.location_id.id,
2831                     'prod_lot_id': old_value.prod_lot_id.id,
2832                     'package_id': old_value.package_id.id,
2833                     'partner_id': old_value.partner_id.id,
2834                     },
2835                 'warning': {
2836                     'title': _('Error'),
2837                     'message': _('You can only change the checked quantity of an existing inventory line. If you want modify a data, please set the checked quantity to 0 and create a new inventory line.')
2838                 }
2839             }
2840         return {}
2841
2842     def on_change_product_id(self, cr, uid, ids, product, uom, theoretical_qty, context=None):
2843         """ Changes UoM
2844         @param location_id: Location id
2845         @param product: Changed product_id
2846         @param uom: UoM product
2847         @return:  Dictionary of changed values
2848         """
2849         if ids and theoretical_qty:
2850             return self.restrict_change(cr, uid, ids, theoretical_qty, context=context)
2851         if not product:
2852             return {'value': {'product_uom_id': False}}
2853         obj_product = self.pool.get('product.product').browse(cr, uid, product, context=context)
2854         return {'value': {'product_uom_id': uom or obj_product.uom_id.id}}
2855
2856
2857 #----------------------------------------------------------
2858 # Stock Warehouse
2859 #----------------------------------------------------------
2860 class stock_warehouse(osv.osv):
2861     _name = "stock.warehouse"
2862     _description = "Warehouse"
2863
2864     _columns = {
2865         'name': fields.char('Warehouse Name', required=True, select=True),
2866         'company_id': fields.many2one('res.company', 'Company', required=True, readonly=True, select=True),
2867         'partner_id': fields.many2one('res.partner', 'Address'),
2868         'view_location_id': fields.many2one('stock.location', 'View Location', required=True, domain=[('usage', '=', 'view')]),
2869         'lot_stock_id': fields.many2one('stock.location', 'Location Stock', domain=[('usage', '=', 'internal')], required=True),
2870         'code': fields.char('Short Name', size=5, required=True, help="Short name used to identify your warehouse"),
2871         'route_ids': fields.many2many('stock.location.route', 'stock_route_warehouse', 'warehouse_id', 'route_id', 'Routes', domain="[('warehouse_selectable', '=', True)]", help='Defaults routes through the warehouse'),
2872         'reception_steps': fields.selection([
2873             ('one_step', 'Receive goods directly in stock (1 step)'),
2874             ('two_steps', 'Unload in input location then go to stock (2 steps)'),
2875             ('three_steps', 'Unload in input location, go through a quality control before being admitted in stock (3 steps)')], 'Incoming Shipments', 
2876                                             help="Default incoming route to follow", required=True),
2877         'delivery_steps': fields.selection([
2878             ('ship_only', 'Ship directly from stock (Ship only)'),
2879             ('pick_ship', 'Bring goods to output location before shipping (Pick + Ship)'),
2880             ('pick_pack_ship', 'Make packages into a dedicated location, then bring them to the output location for shipping (Pick + Pack + Ship)')], 'Outgoing Shippings', 
2881                                            help="Default outgoing route to follow", required=True),
2882         'wh_input_stock_loc_id': fields.many2one('stock.location', 'Input Location'),
2883         'wh_qc_stock_loc_id': fields.many2one('stock.location', 'Quality Control Location'),
2884         'wh_output_stock_loc_id': fields.many2one('stock.location', 'Output Location'),
2885         'wh_pack_stock_loc_id': fields.many2one('stock.location', 'Packing Location'),
2886         'mto_pull_id': fields.many2one('procurement.rule', 'MTO rule'),
2887         'pick_type_id': fields.many2one('stock.picking.type', 'Pick Type'),
2888         'pack_type_id': fields.many2one('stock.picking.type', 'Pack Type'),
2889         'out_type_id': fields.many2one('stock.picking.type', 'Out Type'),
2890         'in_type_id': fields.many2one('stock.picking.type', 'In Type'),
2891         'int_type_id': fields.many2one('stock.picking.type', 'Internal Type'),
2892         'crossdock_route_id': fields.many2one('stock.location.route', 'Crossdock Route'),
2893         'reception_route_id': fields.many2one('stock.location.route', 'Receipt Route'),
2894         'delivery_route_id': fields.many2one('stock.location.route', 'Delivery Route'),
2895         'resupply_from_wh': fields.boolean('Resupply From Other Warehouses'),
2896         'resupply_wh_ids': fields.many2many('stock.warehouse', 'stock_wh_resupply_table', 'supplied_wh_id', 'supplier_wh_id', 'Resupply Warehouses'),
2897         'resupply_route_ids': fields.one2many('stock.location.route', 'supplied_wh_id', 'Resupply Routes', 
2898                                               help="Routes will be created for these resupply warehouses and you can select them on products and product categories"),
2899         'default_resupply_wh_id': fields.many2one('stock.warehouse', 'Default Resupply Warehouse', help="Goods will always be resupplied from this warehouse"),
2900     }
2901
2902     def onchange_filter_default_resupply_wh_id(self, cr, uid, ids, default_resupply_wh_id, resupply_wh_ids, context=None):
2903         resupply_wh_ids = set([x['id'] for x in (self.resolve_2many_commands(cr, uid, 'resupply_wh_ids', resupply_wh_ids, ['id']))])
2904         if default_resupply_wh_id: #If we are removing the default resupply, we don't have default_resupply_wh_id 
2905             resupply_wh_ids.add(default_resupply_wh_id)
2906         resupply_wh_ids = list(resupply_wh_ids)        
2907         return {'value': {'resupply_wh_ids': resupply_wh_ids}}
2908
2909     def _get_external_transit_location(self, cr, uid, warehouse, context=None):
2910         ''' returns browse record of inter company transit location, if found'''
2911         data_obj = self.pool.get('ir.model.data')
2912         location_obj = self.pool.get('stock.location')
2913         try:
2914             inter_wh_loc = data_obj.get_object_reference(cr, uid, 'stock', 'stock_location_inter_wh')[1]
2915         except:
2916             return False
2917         return location_obj.browse(cr, uid, inter_wh_loc, context=context)
2918
2919     def _get_inter_wh_route(self, cr, uid, warehouse, wh, context=None):
2920         return {
2921             'name': _('%s: Supply Product from %s') % (warehouse.name, wh.name),
2922             'warehouse_selectable': False,
2923             'product_selectable': True,
2924             'product_categ_selectable': True,
2925             'supplied_wh_id': warehouse.id,
2926             'supplier_wh_id': wh.id,
2927         }
2928
2929     def _create_resupply_routes(self, cr, uid, warehouse, supplier_warehouses, default_resupply_wh, context=None):
2930         route_obj = self.pool.get('stock.location.route')
2931         pull_obj = self.pool.get('procurement.rule')
2932         #create route selectable on the product to resupply the warehouse from another one
2933         external_transit_location = self._get_external_transit_location(cr, uid, warehouse, context=context)
2934         internal_transit_location = warehouse.company_id.internal_transit_location_id
2935         input_loc = warehouse.wh_input_stock_loc_id
2936         if warehouse.reception_steps == 'one_step':
2937             input_loc = warehouse.lot_stock_id
2938         for wh in supplier_warehouses:
2939             transit_location = wh.company_id.id == warehouse.company_id.id and internal_transit_location or external_transit_location
2940             if transit_location:
2941                 output_loc = wh.wh_output_stock_loc_id
2942                 if wh.delivery_steps == 'ship_only':
2943                     output_loc = wh.lot_stock_id
2944                     # Create extra MTO rule (only for 'ship only' because in the other cases MTO rules already exists)
2945                     mto_pull_vals = self._get_mto_pull_rule(cr, uid, wh, [(output_loc, transit_location, wh.out_type_id.id)], context=context)[0]
2946                     pull_obj.create(cr, uid, mto_pull_vals, context=context)
2947                 inter_wh_route_vals = self._get_inter_wh_route(cr, uid, warehouse, wh, context=context)
2948                 inter_wh_route_id = route_obj.create(cr, uid, vals=inter_wh_route_vals, context=context)
2949                 values = [(output_loc, transit_location, wh.out_type_id.id, wh), (transit_location, input_loc, warehouse.in_type_id.id, warehouse)]
2950                 pull_rules_list = self._get_supply_pull_rules(cr, uid, warehouse, values, inter_wh_route_id, context=context)
2951                 for pull_rule in pull_rules_list:
2952                     pull_obj.create(cr, uid, vals=pull_rule, context=context)
2953                 #if the warehouse is also set as default resupply method, assign this route automatically to the warehouse
2954                 if default_resupply_wh and default_resupply_wh.id == wh.id:
2955                     self.write(cr, uid, [warehouse.id], {'route_ids': [(4, inter_wh_route_id)]}, context=context)
2956
2957     _defaults = {
2958         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.inventory', context=c),
2959         'reception_steps': 'one_step',
2960         'delivery_steps': 'ship_only',
2961     }
2962     _sql_constraints = [
2963         ('warehouse_name_uniq', 'unique(name, company_id)', 'The name of the warehouse must be unique per company!'),
2964         ('warehouse_code_uniq', 'unique(code, company_id)', 'The code of the warehouse must be unique per company!'),
2965     ]
2966
2967     def _get_partner_locations(self, cr, uid, ids, context=None):
2968         ''' returns a tuple made of the browse record of customer location and the browse record of supplier location'''
2969         data_obj = self.pool.get('ir.model.data')
2970         location_obj = self.pool.get('stock.location')
2971         try:
2972             customer_loc = data_obj.get_object_reference(cr, uid, 'stock', 'stock_location_customers')[1]
2973             supplier_loc = data_obj.get_object_reference(cr, uid, 'stock', 'stock_location_suppliers')[1]
2974         except:
2975             customer_loc = location_obj.search(cr, uid, [('usage', '=', 'customer')], context=context)
2976             customer_loc = customer_loc and customer_loc[0] or False
2977             supplier_loc = location_obj.search(cr, uid, [('usage', '=', 'supplier')], context=context)
2978             supplier_loc = supplier_loc and supplier_loc[0] or False
2979         if not (customer_loc and supplier_loc):
2980             raise osv.except_osv(_('Error!'), _('Can\'t find any customer or supplier location.'))
2981         return location_obj.browse(cr, uid, [customer_loc, supplier_loc], context=context)
2982
2983     def switch_location(self, cr, uid, ids, warehouse, new_reception_step=False, new_delivery_step=False, context=None):
2984         location_obj = self.pool.get('stock.location')
2985
2986         new_reception_step = new_reception_step or warehouse.reception_steps
2987         new_delivery_step = new_delivery_step or warehouse.delivery_steps
2988         if warehouse.reception_steps != new_reception_step:
2989             location_obj.write(cr, uid, [warehouse.wh_input_stock_loc_id.id, warehouse.wh_qc_stock_loc_id.id], {'active': False}, context=context)
2990             if new_reception_step != 'one_step':
2991                 location_obj.write(cr, uid, warehouse.wh_input_stock_loc_id.id, {'active': True}, context=context)
2992             if new_reception_step == 'three_steps':
2993                 location_obj.write(cr, uid, warehouse.wh_qc_stock_loc_id.id, {'active': True}, context=context)
2994
2995         if warehouse.delivery_steps != new_delivery_step:
2996             location_obj.write(cr, uid, [warehouse.wh_output_stock_loc_id.id, warehouse.wh_pack_stock_loc_id.id], {'active': False}, context=context)
2997             if new_delivery_step != 'ship_only':
2998                 location_obj.write(cr, uid, warehouse.wh_output_stock_loc_id.id, {'active': True}, context=context)
2999             if new_delivery_step == 'pick_pack_ship':
3000                 location_obj.write(cr, uid, warehouse.wh_pack_stock_loc_id.id, {'active': True}, context=context)
3001         return True
3002
3003     def _get_reception_delivery_route(self, cr, uid, warehouse, route_name, context=None):
3004         return {
3005             'name': self._format_routename(cr, uid, warehouse, route_name, context=context),
3006             'product_categ_selectable': True,
3007             'product_selectable': False,
3008             'sequence': 10,
3009         }
3010
3011     def _get_supply_pull_rules(self, cr, uid, supplied_warehouse, values, new_route_id, context=None):
3012         pull_rules_list = []
3013         for from_loc, dest_loc, pick_type_id, warehouse in values:
3014             pull_rules_list.append({
3015                 'name': self._format_rulename(cr, uid, warehouse, from_loc, dest_loc, context=context),
3016                 'location_src_id': from_loc.id,
3017                 'location_id': dest_loc.id,
3018                 'route_id': new_route_id,
3019                 'action': 'move',
3020                 'picking_type_id': pick_type_id,
3021                 'procure_method': warehouse.lot_stock_id.id != from_loc.id and 'make_to_order' or 'make_to_stock', # first part of the resuply route is MTS
3022                 'warehouse_id': supplied_warehouse.id,
3023                 'propagate_warehouse_id': warehouse.id,
3024             })
3025         return pull_rules_list
3026
3027     def _get_push_pull_rules(self, cr, uid, warehouse, active, values, new_route_id, context=None):
3028         first_rule = True
3029         push_rules_list = []
3030         pull_rules_list = []
3031         for from_loc, dest_loc, pick_type_id in values:
3032             push_rules_list.append({
3033                 'name': self._format_rulename(cr, uid, warehouse, from_loc, dest_loc, context=context),
3034                 'location_from_id': from_loc.id,
3035                 'location_dest_id': dest_loc.id,
3036                 'route_id': new_route_id,
3037                 'auto': 'manual',
3038                 'picking_type_id': pick_type_id,
3039                 'active': active,
3040                 'warehouse_id': warehouse.id,
3041             })
3042             pull_rules_list.append({
3043                 'name': self._format_rulename(cr, uid, warehouse, from_loc, dest_loc, context=context),
3044                 'location_src_id': from_loc.id,
3045                 'location_id': dest_loc.id,
3046                 'route_id': new_route_id,
3047                 'action': 'move',
3048                 'picking_type_id': pick_type_id,
3049                 'procure_method': first_rule is True and 'make_to_stock' or 'make_to_order',
3050                 'active': active,
3051                 'warehouse_id': warehouse.id,
3052             })
3053             first_rule = False
3054         return push_rules_list, pull_rules_list
3055
3056     def _get_mto_route(self, cr, uid, context=None):
3057         route_obj = self.pool.get('stock.location.route')
3058         data_obj = self.pool.get('ir.model.data')
3059         try:
3060             mto_route_id = data_obj.get_object_reference(cr, uid, 'stock', 'route_warehouse0_mto')[1]
3061         except:
3062             mto_route_id = route_obj.search(cr, uid, [('name', 'like', _('Make To Order'))], context=context)
3063             mto_route_id = mto_route_id and mto_route_id[0] or False
3064         if not mto_route_id:
3065             raise osv.except_osv(_('Error!'), _('Can\'t find any generic Make To Order route.'))
3066         return mto_route_id
3067
3068     def _check_remove_mto_resupply_rules(self, cr, uid, warehouse, context=None):
3069         """ Checks that the moves from the different """
3070         pull_obj = self.pool.get('procurement.rule')
3071         mto_route_id = self._get_mto_route(cr, uid, context=context)
3072         rules = pull_obj.search(cr, uid, ['&', ('location_src_id', '=', warehouse.lot_stock_id.id), ('location_id.usage', '=', 'transit')], context=context)
3073         pull_obj.unlink(cr, uid, rules, context=context)
3074
3075     def _get_mto_pull_rule(self, cr, uid, warehouse, values, context=None):
3076         mto_route_id = self._get_mto_route(cr, uid, context=context)
3077         res = []
3078         for value in values:
3079             from_loc, dest_loc, pick_type_id = value
3080             res += [{
3081             'name': self._format_rulename(cr, uid, warehouse, from_loc, dest_loc, context=context) + _(' MTO'),
3082             'location_src_id': from_loc.id,
3083             'location_id': dest_loc.id,
3084             'route_id': mto_route_id,
3085             'action': 'move',
3086             'picking_type_id': pick_type_id,
3087             'procure_method': 'make_to_order',
3088             'active': True,
3089             'warehouse_id': warehouse.id,
3090             }]
3091         return res
3092
3093     def _get_crossdock_route(self, cr, uid, warehouse, route_name, context=None):
3094         return {
3095             'name': self._format_routename(cr, uid, warehouse, route_name, context=context),
3096             'warehouse_selectable': False,
3097             'product_selectable': True,
3098             'product_categ_selectable': True,
3099             'active': warehouse.delivery_steps != 'ship_only' and warehouse.reception_steps != 'one_step',
3100             'sequence': 20,
3101         }
3102
3103     def create_routes(self, cr, uid, ids, warehouse, context=None):
3104         wh_route_ids = []
3105         route_obj = self.pool.get('stock.location.route')
3106         pull_obj = self.pool.get('procurement.rule')
3107         push_obj = self.pool.get('stock.location.path')
3108         routes_dict = self.get_routes_dict(cr, uid, ids, warehouse, context=context)
3109         #create reception route and rules
3110         route_name, values = routes_dict[warehouse.reception_steps]
3111         route_vals = self._get_reception_delivery_route(cr, uid, warehouse, route_name, context=context)
3112         reception_route_id = route_obj.create(cr, uid, route_vals, context=context)
3113         wh_route_ids.append((4, reception_route_id))
3114         push_rules_list, pull_rules_list = self._get_push_pull_rules(cr, uid, warehouse, True, values, reception_route_id, context=context)
3115         #create the push/pull rules
3116         for push_rule in push_rules_list:
3117             push_obj.create(cr, uid, vals=push_rule, context=context)
3118         for pull_rule in pull_rules_list:
3119             #all pull rules in reception route are mto, because we don't want to wait for the scheduler to trigger an orderpoint on input location
3120             pull_rule['procure_method'] = 'make_to_order'
3121             pull_obj.create(cr, uid, vals=pull_rule, context=context)
3122
3123         #create MTS route and pull rules for delivery and a specific route MTO to be set on the product
3124         route_name, values = routes_dict[warehouse.delivery_steps]
3125         route_vals = self._get_reception_delivery_route(cr, uid, warehouse, route_name, context=context)
3126         #create the route and its pull rules
3127         delivery_route_id = route_obj.create(cr, uid, route_vals, context=context)
3128         wh_route_ids.append((4, delivery_route_id))
3129         dummy, pull_rules_list = self._get_push_pull_rules(cr, uid, warehouse, True, values, delivery_route_id, context=context)
3130         for pull_rule in pull_rules_list:
3131             pull_obj.create(cr, uid, vals=pull_rule, context=context)
3132         #create MTO pull rule and link it to the generic MTO route
3133         mto_pull_vals = self._get_mto_pull_rule(cr, uid, warehouse, values, context=context)[0]
3134         mto_pull_id = pull_obj.create(cr, uid, mto_pull_vals, context=context)
3135
3136         #create a route for cross dock operations, that can be set on products and product categories
3137         route_name, values = routes_dict['crossdock']
3138         crossdock_route_vals = self._get_crossdock_route(cr, uid, warehouse, route_name, context=context)
3139         crossdock_route_id = route_obj.create(cr, uid, vals=crossdock_route_vals, context=context)
3140         wh_route_ids.append((4, crossdock_route_id))
3141         dummy, pull_rules_list = self._get_push_pull_rules(cr, uid, warehouse, warehouse.delivery_steps != 'ship_only' and warehouse.reception_steps != 'one_step', values, crossdock_route_id, context=context)
3142         for pull_rule in pull_rules_list:
3143             # Fixed cross-dock is logically mto
3144             pull_rule['procure_method'] = 'make_to_order'
3145             pull_obj.create(cr, uid, vals=pull_rule, context=context)
3146
3147         #create route selectable on the product to resupply the warehouse from another one
3148         self._create_resupply_routes(cr, uid, warehouse, warehouse.resupply_wh_ids, warehouse.default_resupply_wh_id, context=context)
3149
3150         #return routes and mto pull rule to store on the warehouse
3151         return {
3152             'route_ids': wh_route_ids,
3153             'mto_pull_id': mto_pull_id,
3154             'reception_route_id': reception_route_id,
3155             'delivery_route_id': delivery_route_id,
3156             'crossdock_route_id': crossdock_route_id,
3157         }
3158
3159     def change_route(self, cr, uid, ids, warehouse, new_reception_step=False, new_delivery_step=False, context=None):
3160         picking_type_obj = self.pool.get('stock.picking.type')
3161         pull_obj = self.pool.get('procurement.rule')
3162         push_obj = self.pool.get('stock.location.path')
3163         route_obj = self.pool.get('stock.location.route')
3164         new_reception_step = new_reception_step or warehouse.reception_steps
3165         new_delivery_step = new_delivery_step or warehouse.delivery_steps
3166
3167         #change the default source and destination location and (de)activate picking types
3168         input_loc = warehouse.wh_input_stock_loc_id
3169         if new_reception_step == 'one_step':
3170             input_loc = warehouse.lot_stock_id
3171         output_loc = warehouse.wh_output_stock_loc_id
3172         if new_delivery_step == 'ship_only':
3173             output_loc = warehouse.lot_stock_id
3174         picking_type_obj.write(cr, uid, warehouse.in_type_id.id, {'default_location_dest_id': input_loc.id}, context=context)
3175         picking_type_obj.write(cr, uid, warehouse.out_type_id.id, {'default_location_src_id': output_loc.id}, context=context)
3176         picking_type_obj.write(cr, uid, warehouse.pick_type_id.id, {'active': new_delivery_step != 'ship_only'}, context=context)
3177         picking_type_obj.write(cr, uid, warehouse.pack_type_id.id, {'active': new_delivery_step == 'pick_pack_ship'}, context=context)
3178
3179         routes_dict = self.get_routes_dict(cr, uid, ids, warehouse, context=context)
3180         #update delivery route and rules: unlink the existing rules of the warehouse delivery route and recreate it
3181         pull_obj.unlink(cr, uid, [pu.id for pu in warehouse.delivery_route_id.pull_ids], context=context)
3182         route_name, values = routes_dict[new_delivery_step]
3183         route_obj.write(cr, uid, warehouse.delivery_route_id.id, {'name': self._format_routename(cr, uid, warehouse, route_name, context=context)}, context=context)
3184         dummy, pull_rules_list = self._get_push_pull_rules(cr, uid, warehouse, True, values, warehouse.delivery_route_id.id, context=context)
3185         #create the pull rules
3186         for pull_rule in pull_rules_list:
3187             pull_obj.create(cr, uid, vals=pull_rule, context=context)
3188
3189         #update receipt route and rules: unlink the existing rules of the warehouse receipt route and recreate it
3190         pull_obj.unlink(cr, uid, [pu.id for pu in warehouse.reception_route_id.pull_ids], context=context)
3191         push_obj.unlink(cr, uid, [pu.id for pu in warehouse.reception_route_id.push_ids], context=context)
3192         route_name, values = routes_dict[new_reception_step]
3193         route_obj.write(cr, uid, warehouse.reception_route_id.id, {'name': self._format_routename(cr, uid, warehouse, route_name, context=context)}, context=context)
3194         push_rules_list, pull_rules_list = self._get_push_pull_rules(cr, uid, warehouse, True, values, warehouse.reception_route_id.id, context=context)
3195         #create the push/pull rules
3196         for push_rule in push_rules_list:
3197             push_obj.create(cr, uid, vals=push_rule, context=context)
3198         for pull_rule in pull_rules_list:
3199             #all pull rules in receipt route are mto, because we don't want to wait for the scheduler to trigger an orderpoint on input location
3200             pull_rule['procure_method'] = 'make_to_order'
3201             pull_obj.create(cr, uid, vals=pull_rule, context=context)
3202
3203         route_obj.write(cr, uid, warehouse.crossdock_route_id.id, {'active': new_reception_step != 'one_step' and new_delivery_step != 'ship_only'}, context=context)
3204
3205         #change MTO rule
3206         dummy, values = routes_dict[new_delivery_step]
3207         mto_pull_vals = self._get_mto_pull_rule(cr, uid, warehouse, values, context=context)[0]
3208         pull_obj.write(cr, uid, warehouse.mto_pull_id.id, mto_pull_vals, context=context)
3209         return True
3210
3211     def create_sequences_and_picking_types(self, cr, uid, warehouse, context=None):
3212         seq_obj = self.pool.get('ir.sequence')
3213         picking_type_obj = self.pool.get('stock.picking.type')
3214         #create new sequences
3215         in_seq_id = seq_obj.create(cr, SUPERUSER_ID, values={'name': warehouse.name + _(' Sequence in'), 'prefix': warehouse.code + '/IN/', 'padding': 5}, context=context)
3216         out_seq_id = seq_obj.create(cr, SUPERUSER_ID, values={'name': warehouse.name + _(' Sequence out'), 'prefix': warehouse.code + '/OUT/', 'padding': 5}, context=context)
3217         pack_seq_id = seq_obj.create(cr, SUPERUSER_ID, values={'name': warehouse.name + _(' Sequence packing'), 'prefix': warehouse.code + '/PACK/', 'padding': 5}, context=context)
3218         pick_seq_id = seq_obj.create(cr, SUPERUSER_ID, values={'name': warehouse.name + _(' Sequence picking'), 'prefix': warehouse.code + '/PICK/', 'padding': 5}, context=context)
3219         int_seq_id = seq_obj.create(cr, SUPERUSER_ID, values={'name': warehouse.name + _(' Sequence internal'), 'prefix': warehouse.code + '/INT/', 'padding': 5}, context=context)
3220
3221         wh_stock_loc = warehouse.lot_stock_id
3222         wh_input_stock_loc = warehouse.wh_input_stock_loc_id
3223         wh_output_stock_loc = warehouse.wh_output_stock_loc_id
3224         wh_pack_stock_loc = warehouse.wh_pack_stock_loc_id
3225
3226         #fetch customer and supplier locations, for references
3227         customer_loc, supplier_loc = self._get_partner_locations(cr, uid, warehouse.id, context=context)
3228
3229         #create in, out, internal picking types for warehouse
3230         input_loc = wh_input_stock_loc
3231         if warehouse.reception_steps == 'one_step':
3232             input_loc = wh_stock_loc
3233         output_loc = wh_output_stock_loc
3234         if warehouse.delivery_steps == 'ship_only':
3235             output_loc = wh_stock_loc
3236
3237         #choose the next available color for the picking types of this warehouse
3238         color = 0
3239         available_colors = [c%9 for c in range(3, 12)]  # put flashy colors first
3240         all_used_colors = self.pool.get('stock.picking.type').search_read(cr, uid, [('warehouse_id', '!=', False), ('color', '!=', False)], ['color'], order='color')
3241         #don't use sets to preserve the list order
3242         for x in all_used_colors:
3243             if x['color'] in available_colors:
3244                 available_colors.remove(x['color'])
3245         if available_colors:
3246             color = available_colors[0]
3247
3248         #order the picking types with a sequence allowing to have the following suit for each warehouse: reception, internal, pick, pack, ship. 
3249         max_sequence = self.pool.get('stock.picking.type').search_read(cr, uid, [], ['sequence'], order='sequence desc')
3250         max_sequence = max_sequence and max_sequence[0]['sequence'] or 0
3251
3252         in_type_id = picking_type_obj.create(cr, uid, vals={
3253             'name': _('Receipts'),
3254             'warehouse_id': warehouse.id,
3255             'code': 'incoming',
3256             'sequence_id': in_seq_id,
3257             'default_location_src_id': supplier_loc.id,
3258             'default_location_dest_id': input_loc.id,
3259             'sequence': max_sequence + 1,
3260             'color': color}, context=context)
3261         out_type_id = picking_type_obj.create(cr, uid, vals={
3262             'name': _('Delivery Orders'),
3263             'warehouse_id': warehouse.id,
3264             'code': 'outgoing',
3265             'sequence_id': out_seq_id,
3266             'return_picking_type_id': in_type_id,
3267             'default_location_src_id': output_loc.id,
3268             'default_location_dest_id': customer_loc.id,
3269             'sequence': max_sequence + 4,
3270             'color': color}, context=context)
3271         picking_type_obj.write(cr, uid, [in_type_id], {'return_picking_type_id': out_type_id}, context=context)
3272         int_type_id = picking_type_obj.create(cr, uid, vals={
3273             'name': _('Internal Transfers'),
3274             'warehouse_id': warehouse.id,
3275             'code': 'internal',
3276             'sequence_id': int_seq_id,
3277             'default_location_src_id': wh_stock_loc.id,
3278             'default_location_dest_id': wh_stock_loc.id,
3279             'active': True,
3280             'sequence': max_sequence + 2,
3281             'color': color}, context=context)
3282         pack_type_id = picking_type_obj.create(cr, uid, vals={
3283             'name': _('Pack'),
3284             'warehouse_id': warehouse.id,
3285             'code': 'internal',
3286             'sequence_id': pack_seq_id,
3287             'default_location_src_id': wh_pack_stock_loc.id,
3288             'default_location_dest_id': output_loc.id,
3289             'active': warehouse.delivery_steps == 'pick_pack_ship',
3290             'sequence': max_sequence + 3,
3291             'color': color}, context=context)
3292         pick_type_id = picking_type_obj.create(cr, uid, vals={
3293             'name': _('Pick'),
3294             'warehouse_id': warehouse.id,
3295             'code': 'internal',
3296             'sequence_id': pick_seq_id,
3297             'default_location_src_id': wh_stock_loc.id,
3298             'default_location_dest_id': wh_pack_stock_loc.id,
3299             'active': warehouse.delivery_steps != 'ship_only',
3300             'sequence': max_sequence + 2,
3301             'color': color}, context=context)
3302
3303         #write picking types on WH
3304         vals = {
3305             'in_type_id': in_type_id,
3306             'out_type_id': out_type_id,
3307             'pack_type_id': pack_type_id,
3308             'pick_type_id': pick_type_id,
3309             'int_type_id': int_type_id,
3310         }
3311         super(stock_warehouse, self).write(cr, uid, warehouse.id, vals=vals, context=context)
3312
3313
3314     def create(self, cr, uid, vals, context=None):
3315         if context is None:
3316             context = {}
3317         if vals is None:
3318             vals = {}
3319         data_obj = self.pool.get('ir.model.data')
3320         seq_obj = self.pool.get('ir.sequence')
3321         picking_type_obj = self.pool.get('stock.picking.type')
3322         location_obj = self.pool.get('stock.location')
3323
3324         #create view location for warehouse
3325         loc_vals = {
3326                 'name': _(vals.get('code')),
3327                 'usage': 'view',
3328                 'location_id': data_obj.get_object_reference(cr, uid, 'stock', 'stock_location_locations')[1],
3329         }
3330         if vals.get('company_id'):
3331             loc_vals['company_id'] = vals.get('company_id')
3332         wh_loc_id = location_obj.create(cr, uid, loc_vals, context=context)
3333         vals['view_location_id'] = wh_loc_id
3334         #create all location
3335         def_values = self.default_get(cr, uid, {'reception_steps', 'delivery_steps'})
3336         reception_steps = vals.get('reception_steps',  def_values['reception_steps'])
3337         delivery_steps = vals.get('delivery_steps', def_values['delivery_steps'])
3338         context_with_inactive = context.copy()
3339         context_with_inactive['active_test'] = False
3340         sub_locations = [
3341             {'name': _('Stock'), 'active': True, 'field': 'lot_stock_id'},
3342             {'name': _('Input'), 'active': reception_steps != 'one_step', 'field': 'wh_input_stock_loc_id'},
3343             {'name': _('Quality Control'), 'active': reception_steps == 'three_steps', 'field': 'wh_qc_stock_loc_id'},
3344             {'name': _('Output'), 'active': delivery_steps != 'ship_only', 'field': 'wh_output_stock_loc_id'},
3345             {'name': _('Packing Zone'), 'active': delivery_steps == 'pick_pack_ship', 'field': 'wh_pack_stock_loc_id'},
3346         ]
3347         for values in sub_locations:
3348             loc_vals = {
3349                 'name': values['name'],
3350                 'usage': 'internal',
3351                 'location_id': wh_loc_id,
3352                 'active': values['active'],
3353             }
3354             if vals.get('company_id'):
3355                 loc_vals['company_id'] = vals.get('company_id')
3356             location_id = location_obj.create(cr, uid, loc_vals, context=context_with_inactive)
3357             vals[values['field']] = location_id
3358
3359         #create WH
3360         new_id = super(stock_warehouse, self).create(cr, uid, vals=vals, context=context)
3361         warehouse = self.browse(cr, uid, new_id, context=context)
3362         self.create_sequences_and_picking_types(cr, uid, warehouse, context=context)
3363         warehouse.refresh()
3364
3365         #create routes and push/pull rules
3366         new_objects_dict = self.create_routes(cr, uid, new_id, warehouse, context=context)
3367         self.write(cr, uid, warehouse.id, new_objects_dict, context=context)
3368         return new_id
3369
3370     def _format_rulename(self, cr, uid, obj, from_loc, dest_loc, context=None):
3371         return obj.code + ': ' + from_loc.name + ' -> ' + dest_loc.name
3372
3373     def _format_routename(self, cr, uid, obj, name, context=None):
3374         return obj.name + ': ' + name
3375
3376     def get_routes_dict(self, cr, uid, ids, warehouse, context=None):
3377         #fetch customer and supplier locations, for references
3378         customer_loc, supplier_loc = self._get_partner_locations(cr, uid, ids, context=context)
3379
3380         return {
3381             'one_step': (_('Receipt in 1 step'), []),
3382             'two_steps': (_('Receipt in 2 steps'), [(warehouse.wh_input_stock_loc_id, warehouse.lot_stock_id, warehouse.int_type_id.id)]),
3383             'three_steps': (_('Receipt in 3 steps'), [(warehouse.wh_input_stock_loc_id, warehouse.wh_qc_stock_loc_id, warehouse.int_type_id.id), (warehouse.wh_qc_stock_loc_id, warehouse.lot_stock_id, warehouse.int_type_id.id)]),
3384             'crossdock': (_('Cross-Dock'), [(warehouse.wh_input_stock_loc_id, warehouse.wh_output_stock_loc_id, warehouse.int_type_id.id), (warehouse.wh_output_stock_loc_id, customer_loc, warehouse.out_type_id.id)]),
3385             'ship_only': (_('Ship Only'), [(warehouse.lot_stock_id, customer_loc, warehouse.out_type_id.id)]),
3386             'pick_ship': (_('Pick + Ship'), [(warehouse.lot_stock_id, warehouse.wh_output_stock_loc_id, warehouse.pick_type_id.id), (warehouse.wh_output_stock_loc_id, customer_loc, warehouse.out_type_id.id)]),
3387             'pick_pack_ship': (_('Pick + Pack + Ship'), [(warehouse.lot_stock_id, warehouse.wh_pack_stock_loc_id, warehouse.pick_type_id.id), (warehouse.wh_pack_stock_loc_id, warehouse.wh_output_stock_loc_id, warehouse.pack_type_id.id), (warehouse.wh_output_stock_loc_id, customer_loc, warehouse.out_type_id.id)]),
3388         }
3389
3390     def _handle_renaming(self, cr, uid, warehouse, name, code, context=None):
3391         location_obj = self.pool.get('stock.location')
3392         route_obj = self.pool.get('stock.location.route')
3393         pull_obj = self.pool.get('procurement.rule')
3394         push_obj = self.pool.get('stock.location.path')
3395         #rename location
3396         location_id = warehouse.lot_stock_id.location_id.id
3397         location_obj.write(cr, uid, location_id, {'name': code}, context=context)
3398         #rename route and push-pull rules
3399         for route in warehouse.route_ids:
3400             route_obj.write(cr, uid, route.id, {'name': route.name.replace(warehouse.name, name, 1)}, context=context)
3401             for pull in route.pull_ids:
3402                 pull_obj.write(cr, uid, pull.id, {'name': pull.name.replace(warehouse.name, name, 1)}, context=context)
3403             for push in route.push_ids:
3404                 push_obj.write(cr, uid, push.id, {'name': pull.name.replace(warehouse.name, name, 1)}, context=context)
3405         #change the mto pull rule name
3406         if warehouse.mto_pull_id.id:
3407             pull_obj.write(cr, uid, warehouse.mto_pull_id.id, {'name': warehouse.mto_pull_id.name.replace(warehouse.name, name, 1)}, context=context)
3408
3409     def _check_delivery_resupply(self, cr, uid, warehouse, new_location, change_to_multiple, context=None):
3410         """ Will check if the resupply routes from this warehouse follow the changes of number of delivery steps """
3411         #Check routes that are being delivered by this warehouse and change the rule going to transit location
3412         route_obj = self.pool.get("stock.location.route")
3413         pull_obj = self.pool.get("procurement.rule")
3414         routes = route_obj.search(cr, uid, [('supplier_wh_id','=', warehouse.id)], context=context)
3415         pulls = pull_obj.search(cr, uid, ['&', ('route_id', 'in', routes), ('location_id.usage', '=', 'transit')], context=context)
3416         if pulls:
3417             pull_obj.write(cr, uid, pulls, {'location_src_id': new_location, 'procure_method': change_to_multiple and "make_to_order" or "make_to_stock"}, context=context)
3418         # Create or clean MTO rules
3419         mto_route_id = self._get_mto_route(cr, uid, context=context)
3420         if not change_to_multiple:
3421             # If single delivery we should create the necessary MTO rules for the resupply 
3422             # pulls = pull_obj.search(cr, uid, ['&', ('route_id', '=', mto_route_id), ('location_id.usage', '=', 'transit'), ('location_src_id', '=', warehouse.lot_stock_id.id)], context=context)
3423             pull_recs = pull_obj.browse(cr, uid, pulls, context=context)
3424             transfer_locs = list(set([x.location_id for x in pull_recs]))
3425             vals = [(warehouse.lot_stock_id , x, warehouse.out_type_id.id) for x in transfer_locs]
3426             mto_pull_vals = self._get_mto_pull_rule(cr, uid, warehouse, vals, context=context)
3427             for mto_pull_val in mto_pull_vals:
3428                 pull_obj.create(cr, uid, mto_pull_val, context=context)
3429         else:
3430             # We need to delete all the MTO pull rules, otherwise they risk to be used in the system
3431             pulls = pull_obj.search(cr, uid, ['&', ('route_id', '=', mto_route_id), ('location_id.usage', '=', 'transit'), ('location_src_id', '=', warehouse.lot_stock_id.id)], context=context)
3432             if pulls:
3433                 pull_obj.unlink(cr, uid, pulls, context=context)
3434
3435     def _check_reception_resupply(self, cr, uid, warehouse, new_location, context=None):
3436         """
3437             Will check if the resupply routes to this warehouse follow the changes of number of receipt steps
3438         """
3439         #Check routes that are being delivered by this warehouse and change the rule coming from transit location
3440         route_obj = self.pool.get("stock.location.route")
3441         pull_obj = self.pool.get("procurement.rule")
3442         routes = route_obj.search(cr, uid, [('supplied_wh_id','=', warehouse.id)], context=context)
3443         pulls= pull_obj.search(cr, uid, ['&', ('route_id', 'in', routes), ('location_src_id.usage', '=', 'transit')])
3444         if pulls:
3445             pull_obj.write(cr, uid, pulls, {'location_id': new_location}, context=context)
3446
3447     def _check_resupply(self, cr, uid, warehouse, reception_new, delivery_new, context=None):
3448         if reception_new:
3449             old_val = warehouse.reception_steps
3450             new_val = reception_new
3451             change_to_one = (old_val != 'one_step' and new_val == 'one_step')
3452             change_to_multiple = (old_val == 'one_step' and new_val != 'one_step')
3453             if change_to_one or change_to_multiple:
3454                 new_location = change_to_one and warehouse.lot_stock_id.id or warehouse.wh_input_stock_loc_id.id
3455                 self._check_reception_resupply(cr, uid, warehouse, new_location, context=context)
3456         if delivery_new:
3457             old_val = warehouse.delivery_steps
3458             new_val = delivery_new
3459             change_to_one = (old_val != 'ship_only' and new_val == 'ship_only')
3460             change_to_multiple = (old_val == 'ship_only' and new_val != 'ship_only')
3461             if change_to_one or change_to_multiple:
3462                 new_location = change_to_one and warehouse.lot_stock_id.id or warehouse.wh_output_stock_loc_id.id 
3463                 self._check_delivery_resupply(cr, uid, warehouse, new_location, change_to_multiple, context=context)
3464
3465     def write(self, cr, uid, ids, vals, context=None):
3466         if context is None:
3467             context = {}
3468         if isinstance(ids, (int, long)):
3469             ids = [ids]
3470         seq_obj = self.pool.get('ir.sequence')
3471         route_obj = self.pool.get('stock.location.route')
3472         context_with_inactive = context.copy()
3473         context_with_inactive['active_test'] = False
3474         for warehouse in self.browse(cr, uid, ids, context=context_with_inactive):
3475             #first of all, check if we need to delete and recreate route
3476             if vals.get('reception_steps') or vals.get('delivery_steps'):
3477                 #activate and deactivate location according to reception and delivery option
3478                 self.switch_location(cr, uid, warehouse.id, warehouse, vals.get('reception_steps', False), vals.get('delivery_steps', False), context=context)
3479                 # switch between route
3480                 self.change_route(cr, uid, ids, warehouse, vals.get('reception_steps', False), vals.get('delivery_steps', False), context=context_with_inactive)
3481                 # Check if we need to change something to resupply warehouses and associated MTO rules
3482                 self._check_resupply(cr, uid, warehouse, vals.get('reception_steps'), vals.get('delivery_steps'), context=context)
3483                 warehouse.refresh()
3484             if vals.get('code') or vals.get('name'):
3485                 name = warehouse.name
3486                 #rename sequence
3487                 if vals.get('name'):
3488                     name = vals.get('name', warehouse.name)
3489                 self._handle_renaming(cr, uid, warehouse, name, vals.get('code', warehouse.code), context=context_with_inactive)
3490                 if warehouse.in_type_id:
3491                     seq_obj.write(cr, uid, warehouse.in_type_id.sequence_id.id, {'name': name + _(' Sequence in'), 'prefix': vals.get('code', warehouse.code) + '\IN\\'}, context=context)
3492                     seq_obj.write(cr, uid, warehouse.out_type_id.sequence_id.id, {'name': name + _(' Sequence out'), 'prefix': vals.get('code', warehouse.code) + '\OUT\\'}, context=context)
3493                     seq_obj.write(cr, uid, warehouse.pack_type_id.sequence_id.id, {'name': name + _(' Sequence packing'), 'prefix': vals.get('code', warehouse.code) + '\PACK\\'}, context=context)
3494                     seq_obj.write(cr, uid, warehouse.pick_type_id.sequence_id.id, {'name': name + _(' Sequence picking'), 'prefix': vals.get('code', warehouse.code) + '\PICK\\'}, context=context)
3495                     seq_obj.write(cr, uid, warehouse.int_type_id.sequence_id.id, {'name': name + _(' Sequence internal'), 'prefix': vals.get('code', warehouse.code) + '\INT\\'}, context=context)
3496         if vals.get('resupply_wh_ids') and not vals.get('resupply_route_ids'):
3497             for cmd in vals.get('resupply_wh_ids'):
3498                 if cmd[0] == 6:
3499                     new_ids = set(cmd[2])
3500                     old_ids = set([wh.id for wh in warehouse.resupply_wh_ids])
3501                     to_add_wh_ids = new_ids - old_ids
3502                     if to_add_wh_ids:
3503                         supplier_warehouses = self.browse(cr, uid, list(to_add_wh_ids), context=context)
3504                         self._create_resupply_routes(cr, uid, warehouse, supplier_warehouses, warehouse.default_resupply_wh_id, context=context)
3505                     to_remove_wh_ids = old_ids - new_ids
3506                     if to_remove_wh_ids:
3507                         to_remove_route_ids = route_obj.search(cr, uid, [('supplied_wh_id', '=', warehouse.id), ('supplier_wh_id', 'in', list(to_remove_wh_ids))], context=context)
3508                         if to_remove_route_ids:
3509                             route_obj.unlink(cr, uid, to_remove_route_ids, context=context)
3510                 else:
3511                     #not implemented
3512                     pass
3513         if 'default_resupply_wh_id' in vals:
3514             if vals.get('default_resupply_wh_id') == warehouse.id:
3515                 raise osv.except_osv(_('Warning'),_('The default resupply warehouse should be different than the warehouse itself!'))
3516             if warehouse.default_resupply_wh_id:
3517                 #remove the existing resupplying route on the warehouse
3518                 to_remove_route_ids = route_obj.search(cr, uid, [('supplied_wh_id', '=', warehouse.id), ('supplier_wh_id', '=', warehouse.default_resupply_wh_id.id)], context=context)
3519                 for inter_wh_route_id in to_remove_route_ids:
3520                     self.write(cr, uid, [warehouse.id], {'route_ids': [(3, inter_wh_route_id)]})
3521             if vals.get('default_resupply_wh_id'):
3522                 #assign the new resupplying route on all products
3523                 to_assign_route_ids = route_obj.search(cr, uid, [('supplied_wh_id', '=', warehouse.id), ('supplier_wh_id', '=', vals.get('default_resupply_wh_id'))], context=context)
3524                 for inter_wh_route_id in to_assign_route_ids:
3525                     self.write(cr, uid, [warehouse.id], {'route_ids': [(4, inter_wh_route_id)]})
3526
3527         return super(stock_warehouse, self).write(cr, uid, ids, vals=vals, context=context)
3528
3529     def get_all_routes_for_wh(self, cr, uid, warehouse, context=None):
3530         route_obj = self.pool.get("stock.location.route")
3531         all_routes = [route.id for route in warehouse.route_ids]
3532         all_routes += route_obj.search(cr, uid, [('supplied_wh_id', '=', warehouse.id)], context=context)
3533         all_routes += [warehouse.mto_pull_id.route_id.id]
3534         return all_routes
3535
3536     def view_all_routes_for_wh(self, cr, uid, ids, context=None):
3537         all_routes = []
3538         for wh in self.browse(cr, uid, ids, context=context):
3539             all_routes += self.get_all_routes_for_wh(cr, uid, wh, context=context)
3540
3541         domain = [('id', 'in', all_routes)]
3542         return {
3543             'name': _('Warehouse\'s Routes'),
3544             'domain': domain,
3545             'res_model': 'stock.location.route',
3546             'type': 'ir.actions.act_window',
3547             'view_id': False,
3548             'view_mode': 'tree,form',
3549             'view_type': 'form',
3550             'limit': 20
3551         }
3552
3553
3554 class stock_location_path(osv.osv):
3555     _name = "stock.location.path"
3556     _description = "Pushed Flows"
3557     _order = "name"
3558
3559     def _get_rules(self, cr, uid, ids, context=None):
3560         res = []
3561         for route in self.browse(cr, uid, ids, context=context):
3562             res += [x.id for x in route.push_ids]
3563         return res
3564
3565     _columns = {
3566         'name': fields.char('Operation Name', required=True),
3567         'company_id': fields.many2one('res.company', 'Company'),
3568         'route_id': fields.many2one('stock.location.route', 'Route'),
3569         'location_from_id': fields.many2one('stock.location', 'Source Location', ondelete='cascade', select=1, required=True),
3570         'location_dest_id': fields.many2one('stock.location', 'Destination Location', ondelete='cascade', select=1, required=True),
3571         'delay': fields.integer('Delay (days)', help="Number of days to do this transition"),
3572         'picking_type_id': fields.many2one('stock.picking.type', 'Type of the new Operation', required=True, help="This is the picking type associated with the different pickings"), 
3573         'auto': fields.selection(
3574             [('auto','Automatic Move'), ('manual','Manual Operation'),('transparent','Automatic No Step Added')],
3575             'Automatic Move',
3576             required=True, select=1,
3577             help="This is used to define paths the product has to follow within the location tree.\n" \
3578                 "The 'Automatic Move' value will create a stock move after the current one that will be "\
3579                 "validated automatically. With 'Manual Operation', the stock move has to be validated "\
3580                 "by a worker. With 'Automatic No Step Added', the location is replaced in the original move."
3581             ),
3582         'propagate': fields.boolean('Propagate cancel and split', help='If checked, when the previous move is cancelled or split, the move generated by this move will too'),
3583         'active': fields.boolean('Active', help="If unchecked, it will allow you to hide the rule without removing it."),
3584         'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse'),
3585         'route_sequence': fields.related('route_id', 'sequence', string='Route Sequence',
3586             store={
3587                 'stock.location.route': (_get_rules, ['sequence'], 10),
3588                 'stock.location.path': (lambda self, cr, uid, ids, c={}: ids, ['route_id'], 10),
3589         }),
3590         'sequence': fields.integer('Sequence'),
3591     }
3592     _defaults = {
3593         'auto': 'auto',
3594         'delay': 0,
3595         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'procurement.order', context=c),
3596         'propagate': True,
3597         'active': True,
3598     }
3599
3600     def _prepare_push_apply(self, cr, uid, rule, move, context=None):
3601         newdate = (datetime.strptime(move.date_expected, DEFAULT_SERVER_DATETIME_FORMAT) + relativedelta.relativedelta(days=rule.delay or 0)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
3602         return {
3603                 'location_id': move.location_dest_id.id,
3604                 'location_dest_id': rule.location_dest_id.id,
3605                 'date': newdate,
3606                 'company_id': rule.company_id and rule.company_id.id or False,
3607                 'date_expected': newdate,
3608                 'picking_id': False,
3609                 'picking_type_id': rule.picking_type_id and rule.picking_type_id.id or False,
3610                 'propagate': rule.propagate,
3611                 'push_rule_id': rule.id,
3612                 'warehouse_id': rule.warehouse_id and rule.warehouse_id.id or False,
3613             }
3614
3615     def _apply(self, cr, uid, rule, move, context=None):
3616         move_obj = self.pool.get('stock.move')
3617         newdate = (datetime.strptime(move.date_expected, DEFAULT_SERVER_DATETIME_FORMAT) + relativedelta.relativedelta(days=rule.delay or 0)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
3618         if rule.auto == 'transparent':
3619             old_dest_location = move.location_dest_id.id
3620             move_obj.write(cr, uid, [move.id], {
3621                 'date': newdate,
3622                 'date_expected': newdate,
3623                 'location_dest_id': rule.location_dest_id.id
3624             })
3625             move.refresh()
3626             #avoid looping if a push rule is not well configured
3627             if rule.location_dest_id.id != old_dest_location:
3628                 #call again push_apply to see if a next step is defined
3629                 move_obj._push_apply(cr, uid, [move], context=context)
3630         else:
3631             vals = self._prepare_push_apply(cr, uid, rule, move, context=context)
3632             move_id = move_obj.copy(cr, uid, move.id, vals, context=context)
3633             move_obj.write(cr, uid, [move.id], {
3634                 'move_dest_id': move_id,
3635             })
3636             move_obj.action_confirm(cr, uid, [move_id], context=None)
3637
3638
3639 # -------------------------
3640 # Packaging related stuff
3641 # -------------------------
3642
3643 from openerp.report import report_sxw
3644
3645 class stock_package(osv.osv):
3646     """
3647     These are the packages, containing quants and/or other packages
3648     """
3649     _name = "stock.quant.package"
3650     _description = "Physical Packages"
3651     _parent_name = "parent_id"
3652     _parent_store = True
3653     _parent_order = 'name'
3654     _order = 'parent_left'
3655
3656     def name_get(self, cr, uid, ids, context=None):
3657         res = self._complete_name(cr, uid, ids, 'complete_name', None, context=context)
3658         return res.items()
3659
3660     def _complete_name(self, cr, uid, ids, name, args, context=None):
3661         """ Forms complete name of location from parent location to child location.
3662         @return: Dictionary of values
3663         """
3664         res = {}
3665         for m in self.browse(cr, uid, ids, context=context):
3666             res[m.id] = m.name
3667             parent = m.parent_id
3668             while parent:
3669                 res[m.id] = parent.name + ' / ' + res[m.id]
3670                 parent = parent.parent_id
3671         return res
3672
3673     def _get_packages(self, cr, uid, ids, context=None):
3674         """Returns packages from quants for store"""
3675         res = set()
3676         for quant in self.browse(cr, uid, ids, context=context):
3677             if quant.package_id:
3678                 res.add(quant.package_id.id)
3679         return list(res)
3680
3681     def _get_packages_to_relocate(self, cr, uid, ids, context=None):
3682         res = set()
3683         for pack in self.browse(cr, uid, ids, context=context):
3684             res.add(pack.id)
3685             if pack.parent_id:
3686                 res.add(pack.parent_id.id)
3687         return list(res)
3688
3689     def _get_package_info(self, cr, uid, ids, name, args, context=None):
3690         default_company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
3691         res = dict((res_id, {'location_id': False, 'company_id': default_company_id, 'owner_id': False}) for res_id in ids)
3692         for pack in self.browse(cr, uid, ids, context=context):
3693             if pack.quant_ids:
3694                 res[pack.id]['location_id'] = pack.quant_ids[0].location_id.id
3695                 res[pack.id]['owner_id'] = pack.quant_ids[0].owner_id and pack.quant_ids[0].owner_id.id or False
3696                 res[pack.id]['company_id'] = pack.quant_ids[0].company_id.id
3697             elif pack.children_ids:
3698                 res[pack.id]['location_id'] = pack.children_ids[0].location_id and pack.children_ids[0].location_id.id or False
3699                 res[pack.id]['owner_id'] = pack.children_ids[0].owner_id and pack.children_ids[0].owner_id.id or False
3700                 res[pack.id]['company_id'] = pack.children_ids[0].company_id and pack.children_ids[0].company_id.id or False
3701         return res
3702
3703     _columns = {
3704         'name': fields.char('Package Reference', select=True, copy=False),
3705         'complete_name': fields.function(_complete_name, type='char', string="Package Name",),
3706         'parent_left': fields.integer('Left Parent', select=1),
3707         'parent_right': fields.integer('Right Parent', select=1),
3708         'packaging_id': fields.many2one('product.packaging', 'Packaging', help="This field should be completed only if everything inside the package share the same product, otherwise it doesn't really makes sense.", select=True),
3709         'ul_id': fields.many2one('product.ul', 'Logistic Unit'),
3710         'location_id': fields.function(_get_package_info, type='many2one', relation='stock.location', string='Location', multi="package",
3711                                     store={
3712                                        'stock.quant': (_get_packages, ['location_id'], 10),
3713                                        'stock.quant.package': (_get_packages_to_relocate, ['quant_ids', 'children_ids', 'parent_id'], 10),
3714                                     }, readonly=True, select=True),
3715         'quant_ids': fields.one2many('stock.quant', 'package_id', 'Bulk Content', readonly=True),
3716         'parent_id': fields.many2one('stock.quant.package', 'Parent Package', help="The package containing this item", ondelete='restrict', readonly=True),
3717         'children_ids': fields.one2many('stock.quant.package', 'parent_id', 'Contained Packages', readonly=True),
3718         'company_id': fields.function(_get_package_info, type="many2one", relation='res.company', string='Company', multi="package", 
3719                                     store={
3720                                        'stock.quant': (_get_packages, ['company_id'], 10),
3721                                        'stock.quant.package': (_get_packages_to_relocate, ['quant_ids', 'children_ids', 'parent_id'], 10),
3722                                     }, readonly=True, select=True),
3723         'owner_id': fields.function(_get_package_info, type='many2one', relation='res.partner', string='Owner', multi="package",
3724                                 store={
3725                                        'stock.quant': (_get_packages, ['owner_id'], 10),
3726                                        'stock.quant.package': (_get_packages_to_relocate, ['quant_ids', 'children_ids', 'parent_id'], 10),
3727                                     }, readonly=True, select=True),
3728     }
3729     _defaults = {
3730         'name': lambda self, cr, uid, context: self.pool.get('ir.sequence').get(cr, uid, 'stock.quant.package') or _('Unknown Pack')
3731     }
3732
3733     def _check_location_constraint(self, cr, uid, packs, context=None):
3734         '''checks that all quants in a package are stored in the same location. This function cannot be used
3735            as a constraint because it needs to be checked on pack operations (they may not call write on the
3736            package)
3737         '''
3738         quant_obj = self.pool.get('stock.quant')
3739         for pack in packs:
3740             parent = pack
3741             while parent.parent_id:
3742                 parent = parent.parent_id
3743             quant_ids = self.get_content(cr, uid, [parent.id], context=context)
3744             quants = [x for x in quant_obj.browse(cr, uid, quant_ids, context=context) if x.qty > 0]
3745             location_id = quants and quants[0].location_id.id or False
3746             if not [quant.location_id.id == location_id for quant in quants]:
3747                 raise osv.except_osv(_('Error'), _('Everything inside a package should be in the same location'))
3748         return True
3749
3750     def action_print(self, cr, uid, ids, context=None):
3751         context = dict(context or {}, active_ids=ids)
3752         return self.pool.get("report").get_action(cr, uid, ids, 'stock.report_package_barcode_small', context=context)
3753     
3754     
3755     def unpack(self, cr, uid, ids, context=None):
3756         quant_obj = self.pool.get('stock.quant')
3757         for package in self.browse(cr, uid, ids, context=context):
3758             quant_ids = [quant.id for quant in package.quant_ids]
3759             quant_obj.write(cr, uid, quant_ids, {'package_id': package.parent_id.id or False}, context=context)
3760             children_package_ids = [child_package.id for child_package in package.children_ids]
3761             self.write(cr, uid, children_package_ids, {'parent_id': package.parent_id.id or False}, context=context)
3762         #delete current package since it contains nothing anymore
3763         self.unlink(cr, uid, ids, context=context)
3764         return self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'stock', 'action_package_view', context=context)
3765
3766     def get_content(self, cr, uid, ids, context=None):
3767         child_package_ids = self.search(cr, uid, [('id', 'child_of', ids)], context=context)
3768         return self.pool.get('stock.quant').search(cr, uid, [('package_id', 'in', child_package_ids)], context=context)
3769
3770     def get_content_package(self, cr, uid, ids, context=None):
3771         quants_ids = self.get_content(cr, uid, ids, context=context)
3772         res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'stock', 'quantsact', context=context)
3773         res['domain'] = [('id', 'in', quants_ids)]
3774         return res
3775
3776     def _get_product_total_qty(self, cr, uid, package_record, product_id, context=None):
3777         ''' find the total of given product 'product_id' inside the given package 'package_id'''
3778         quant_obj = self.pool.get('stock.quant')
3779         all_quant_ids = self.get_content(cr, uid, [package_record.id], context=context)
3780         total = 0
3781         for quant in quant_obj.browse(cr, uid, all_quant_ids, context=context):
3782             if quant.product_id.id == product_id:
3783                 total += quant.qty
3784         return total
3785
3786     def _get_all_products_quantities(self, cr, uid, package_id, context=None):
3787         '''This function computes the different product quantities for the given package
3788         '''
3789         quant_obj = self.pool.get('stock.quant')
3790         res = {}
3791         for quant in quant_obj.browse(cr, uid, self.get_content(cr, uid, package_id, context=context)):
3792             if quant.product_id.id not in res:
3793                 res[quant.product_id.id] = 0
3794             res[quant.product_id.id] += quant.qty
3795         return res
3796
3797     def copy_pack(self, cr, uid, id, default_pack_values=None, default=None, context=None):
3798         stock_pack_operation_obj = self.pool.get('stock.pack.operation')
3799         if default is None:
3800             default = {}
3801         new_package_id = self.copy(cr, uid, id, default_pack_values, context=context)
3802         default['result_package_id'] = new_package_id
3803         op_ids = stock_pack_operation_obj.search(cr, uid, [('result_package_id', '=', id)], context=context)
3804         for op_id in op_ids:
3805             stock_pack_operation_obj.copy(cr, uid, op_id, default, context=context)
3806
3807
3808 class stock_pack_operation(osv.osv):
3809     _name = "stock.pack.operation"
3810     _description = "Packing Operation"
3811
3812     def _get_remaining_prod_quantities(self, cr, uid, operation, context=None):
3813         '''Get the remaining quantities per product on an operation with a package. This function returns a dictionary'''
3814         #if the operation doesn't concern a package, it's not relevant to call this function
3815         if not operation.package_id or operation.product_id:
3816             return {operation.product_id.id: operation.remaining_qty}
3817         #get the total of products the package contains
3818         res = self.pool.get('stock.quant.package')._get_all_products_quantities(cr, uid, operation.package_id.id, context=context)
3819         #reduce by the quantities linked to a move
3820         for record in operation.linked_move_operation_ids:
3821             if record.move_id.product_id.id not in res:
3822                 res[record.move_id.product_id.id] = 0
3823             res[record.move_id.product_id.id] -= record.qty
3824         return res
3825
3826     def _get_remaining_qty(self, cr, uid, ids, name, args, context=None):
3827         uom_obj = self.pool.get('product.uom')
3828         res = {}
3829         for ops in self.browse(cr, uid, ids, context=context):
3830             res[ops.id] = 0
3831             if ops.package_id and not ops.product_id:
3832                 #dont try to compute the remaining quantity for packages because it's not relevant (a package could include different products).
3833                 #should use _get_remaining_prod_quantities instead
3834                 continue
3835             else:
3836                 qty = ops.product_qty
3837                 if ops.product_uom_id:
3838                     qty = uom_obj._compute_qty_obj(cr, uid, ops.product_uom_id, ops.product_qty, ops.product_id.uom_id, context=context)
3839                 for record in ops.linked_move_operation_ids:
3840                     qty -= record.qty
3841                 res[ops.id] = qty
3842         return res
3843
3844     def product_id_change(self, cr, uid, ids, product_id, product_uom_id, product_qty, context=None):
3845         res = self.on_change_tests(cr, uid, ids, product_id, product_uom_id, product_qty, context=context)
3846         if product_id and not product_uom_id:
3847             product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
3848             res['value']['product_uom_id'] = product.uom_id.id
3849         return res
3850
3851     def on_change_tests(self, cr, uid, ids, product_id, product_uom_id, product_qty, context=None):
3852         res = {'value': {}}
3853         uom_obj = self.pool.get('product.uom')
3854         if product_id:
3855             product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
3856             product_uom_id = product_uom_id or product.uom_id.id
3857             selected_uom = uom_obj.browse(cr, uid, product_uom_id, context=context)
3858             if selected_uom.category_id.id != product.uom_id.category_id.id:
3859                 res['warning'] = {
3860                     'title': _('Warning: wrong UoM!'),
3861                     'message': _('The selected UoM for product %s is not compatible with the UoM set on the product form. \nPlease choose an UoM within the same UoM category.') % (product.name)
3862                 }
3863             if product_qty and 'warning' not in res:
3864                 rounded_qty = uom_obj._compute_qty(cr, uid, product_uom_id, product_qty, product_uom_id, round=True)
3865                 if rounded_qty != product_qty:
3866                     res['warning'] = {
3867                         'title': _('Warning: wrong quantity!'),
3868                         'message': _('The chosen quantity for product %s is not compatible with the UoM rounding. It will be automatically converted at confirmation') % (product.name)
3869                     }
3870         return res
3871
3872     _columns = {
3873         'picking_id': fields.many2one('stock.picking', 'Stock Picking', help='The stock operation where the packing has been made', required=True),
3874         'product_id': fields.many2one('product.product', 'Product', ondelete="CASCADE"),  # 1
3875         'product_uom_id': fields.many2one('product.uom', 'Product Unit of Measure'),
3876         'product_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
3877         'qty_done': fields.float('Quantity Processed', digits_compute=dp.get_precision('Product Unit of Measure')),
3878         'package_id': fields.many2one('stock.quant.package', 'Source Package'),  # 2
3879         'lot_id': fields.many2one('stock.production.lot', 'Lot/Serial Number'),
3880         'result_package_id': fields.many2one('stock.quant.package', 'Destination Package', help="If set, the operations are packed into this package", required=False, ondelete='cascade'),
3881         'date': fields.datetime('Date', required=True),
3882         'owner_id': fields.many2one('res.partner', 'Owner', help="Owner of the quants"),
3883         #'update_cost': fields.boolean('Need cost update'),
3884         'cost': fields.float("Cost", help="Unit Cost for this product line"),
3885         'currency': fields.many2one('res.currency', string="Currency", help="Currency in which Unit cost is expressed", ondelete='CASCADE'),
3886         'linked_move_operation_ids': fields.one2many('stock.move.operation.link', 'operation_id', string='Linked Moves', readonly=True, help='Moves impacted by this operation for the computation of the remaining quantities'),
3887         'remaining_qty': fields.function(_get_remaining_qty, type='float', digits = 0, string="Remaining Qty", help="Remaining quantity in default UoM according to moves matched with this operation. "),
3888         'location_id': fields.many2one('stock.location', 'Source Location', required=True),
3889         'location_dest_id': fields.many2one('stock.location', 'Destination Location', required=True),
3890         'processed': fields.selection([('true','Yes'), ('false','No')],'Has been processed?', required=True),
3891     }
3892
3893     _defaults = {
3894         'date': fields.date.context_today,
3895         'qty_done': 0,
3896         'processed': lambda *a: 'false',
3897     }
3898
3899     def write(self, cr, uid, ids, vals, context=None):
3900         context = context or {}
3901         res = super(stock_pack_operation, self).write(cr, uid, ids, vals, context=context)
3902         if isinstance(ids, (int, long)):
3903             ids = [ids]
3904         if not context.get("no_recompute"):
3905             pickings = vals.get('picking_id') and [vals['picking_id']] or list(set([x.picking_id.id for x in self.browse(cr, uid, ids, context=context)]))
3906             self.pool.get("stock.picking").do_recompute_remaining_quantities(cr, uid, pickings, context=context)
3907         return res
3908
3909     def create(self, cr, uid, vals, context=None):
3910         context = context or {}
3911         res_id = super(stock_pack_operation, self).create(cr, uid, vals, context=context)
3912         if vals.get("picking_id") and not context.get("no_recompute"):
3913             self.pool.get("stock.picking").do_recompute_remaining_quantities(cr, uid, [vals['picking_id']], context=context)
3914         return res_id
3915
3916     def action_drop_down(self, cr, uid, ids, context=None):
3917         ''' Used by barcode interface to say that pack_operation has been moved from src location 
3918             to destination location, if qty_done is less than product_qty than we have to split the
3919             operation in two to process the one with the qty moved
3920         '''
3921         processed_ids = []
3922         move_obj = self.pool.get("stock.move")
3923         for pack_op in self.browse(cr, uid, ids, context=None):
3924             if pack_op.product_id and pack_op.location_id and pack_op.location_dest_id:
3925                 move_obj.check_tracking_product(cr, uid, pack_op.product_id, pack_op.lot_id.id, pack_op.location_id, pack_op.location_dest_id, context=context)
3926             op = pack_op.id
3927             if pack_op.qty_done < pack_op.product_qty:
3928                 # we split the operation in two
3929                 op = self.copy(cr, uid, pack_op.id, {'product_qty': pack_op.qty_done, 'qty_done': pack_op.qty_done}, context=context)
3930                 self.write(cr, uid, [pack_op.id], {'product_qty': pack_op.product_qty - pack_op.qty_done, 'qty_done': 0, 'lot_id': False}, context=context)
3931             processed_ids.append(op)
3932         self.write(cr, uid, processed_ids, {'processed': 'true'}, context=context)
3933
3934     def create_and_assign_lot(self, cr, uid, id, name, context=None):
3935         ''' Used by barcode interface to create a new lot and assign it to the operation
3936         '''
3937         obj = self.browse(cr,uid,id,context)
3938         product_id = obj.product_id.id
3939         val = {'product_id': product_id}
3940         new_lot_id = False
3941         if name:
3942             lots = self.pool.get('stock.production.lot').search(cr, uid, ['&', ('name', '=', name), ('product_id', '=', product_id)], context=context)
3943             if lots:
3944                 new_lot_id = lots[0]
3945             val.update({'name': name})
3946
3947         if not new_lot_id:
3948             new_lot_id = self.pool.get('stock.production.lot').create(cr, uid, val, context=context)
3949         self.write(cr, uid, id, {'lot_id': new_lot_id}, context=context)
3950
3951     def _search_and_increment(self, cr, uid, picking_id, domain, filter_visible=False, visible_op_ids=False, increment=True, context=None):
3952         '''Search for an operation with given 'domain' in a picking, if it exists increment the qty (+1) otherwise create it
3953
3954         :param domain: list of tuple directly reusable as a domain
3955         context can receive a key 'current_package_id' with the package to consider for this operation
3956         returns True
3957         '''
3958         if context is None:
3959             context = {}
3960
3961         #if current_package_id is given in the context, we increase the number of items in this package
3962         package_clause = [('result_package_id', '=', context.get('current_package_id', False))]
3963         existing_operation_ids = self.search(cr, uid, [('picking_id', '=', picking_id)] + domain + package_clause, context=context)
3964         todo_operation_ids = []
3965         if existing_operation_ids:
3966             if filter_visible:
3967                 todo_operation_ids = [val for val in existing_operation_ids if val in visible_op_ids]
3968             else:
3969                 todo_operation_ids = existing_operation_ids
3970         if todo_operation_ids:
3971             #existing operation found for the given domain and picking => increment its quantity
3972             operation_id = todo_operation_ids[0]
3973             op_obj = self.browse(cr, uid, operation_id, context=context)
3974             qty = op_obj.qty_done
3975             if increment:
3976                 qty += 1
3977             else:
3978                 qty -= 1 if qty >= 1 else 0
3979                 if qty == 0 and op_obj.product_qty == 0:
3980                     #we have a line with 0 qty set, so delete it
3981                     self.unlink(cr, uid, [operation_id], context=context)
3982                     return False
3983             self.write(cr, uid, [operation_id], {'qty_done': qty}, context=context)
3984         else:
3985             #no existing operation found for the given domain and picking => create a new one
3986             picking_obj = self.pool.get("stock.picking")
3987             picking = picking_obj.browse(cr, uid, picking_id, context=context)
3988             values = {
3989                 'picking_id': picking_id,
3990                 'product_qty': 0,
3991                 'location_id': picking.location_id.id, 
3992                 'location_dest_id': picking.location_dest_id.id,
3993                 'qty_done': 1,
3994                 }
3995             for key in domain:
3996                 var_name, dummy, value = key
3997                 uom_id = False
3998                 if var_name == 'product_id':
3999                     uom_id = self.pool.get('product.product').browse(cr, uid, value, context=context).uom_id.id
4000                 update_dict = {var_name: value}
4001                 if uom_id:
4002                     update_dict['product_uom_id'] = uom_id
4003                 values.update(update_dict)
4004             operation_id = self.create(cr, uid, values, context=context)
4005         return operation_id
4006
4007
4008 class stock_move_operation_link(osv.osv):
4009     """
4010     Table making the link between stock.moves and stock.pack.operations to compute the remaining quantities on each of these objects
4011     """
4012     _name = "stock.move.operation.link"
4013     _description = "Link between stock moves and pack operations"
4014
4015     _columns = {
4016         'qty': fields.float('Quantity', help="Quantity of products to consider when talking about the contribution of this pack operation towards the remaining quantity of the move (and inverse). Given in the product main uom."),
4017         'operation_id': fields.many2one('stock.pack.operation', 'Operation', required=True, ondelete="cascade"),
4018         'move_id': fields.many2one('stock.move', 'Move', required=True, ondelete="cascade"),
4019         'reserved_quant_id': fields.many2one('stock.quant', 'Reserved Quant', help="Technical field containing the quant that created this link between an operation and a stock move. Used at the stock_move_obj.action_done() time to avoid seeking a matching quant again"),
4020     }
4021
4022     def get_specific_domain(self, cr, uid, record, context=None):
4023         '''Returns the specific domain to consider for quant selection in action_assign() or action_done() of stock.move,
4024         having the record given as parameter making the link between the stock move and a pack operation'''
4025
4026         op = record.operation_id
4027         domain = []
4028         if op.package_id and op.product_id:
4029             #if removing a product from a box, we restrict the choice of quants to this box
4030             domain.append(('package_id', '=', op.package_id.id))
4031         elif op.package_id:
4032             #if moving a box, we allow to take everything from inside boxes as well
4033             domain.append(('package_id', 'child_of', [op.package_id.id]))
4034         else:
4035             #if not given any information about package, we don't open boxes
4036             domain.append(('package_id', '=', False))
4037         #if lot info is given, we restrict choice to this lot otherwise we can take any
4038         if op.lot_id:
4039             domain.append(('lot_id', '=', op.lot_id.id))
4040         #if owner info is given, we restrict to this owner otherwise we restrict to no owner
4041         if op.owner_id:
4042             domain.append(('owner_id', '=', op.owner_id.id))
4043         else:
4044             domain.append(('owner_id', '=', False))
4045         return domain
4046
4047 class stock_warehouse_orderpoint(osv.osv):
4048     """
4049     Defines Minimum stock rules.
4050     """
4051     _name = "stock.warehouse.orderpoint"
4052     _description = "Minimum Inventory Rule"
4053
4054     def subtract_procurements(self, cr, uid, orderpoint, context=None):
4055         '''This function returns quantity of product that needs to be deducted from the orderpoint computed quantity because there's already a procurement created with aim to fulfill it.
4056         '''
4057         qty = 0
4058         uom_obj = self.pool.get("product.uom")
4059         for procurement in orderpoint.procurement_ids:
4060             if procurement.state in ('cancel', 'done'):
4061                 continue
4062             procurement_qty = uom_obj._compute_qty_obj(cr, uid, procurement.product_uom, procurement.product_qty, procurement.product_id.uom_id, context=context)
4063             for move in procurement.move_ids:
4064                 #need to add the moves in draft as they aren't in the virtual quantity + moves that have not been created yet
4065                 if move.state not in ('draft'):
4066                     #if move is already confirmed, assigned or done, the virtual stock is already taking this into account so it shouldn't be deducted
4067                     procurement_qty -= move.product_qty
4068             qty += procurement_qty
4069         return qty
4070
4071     def _check_product_uom(self, cr, uid, ids, context=None):
4072         '''
4073         Check if the UoM has the same category as the product standard UoM
4074         '''
4075         if not context:
4076             context = {}
4077
4078         for rule in self.browse(cr, uid, ids, context=context):
4079             if rule.product_id.uom_id.category_id.id != rule.product_uom.category_id.id:
4080                 return False
4081         return True
4082
4083     def action_view_proc_to_process(self, cr, uid, ids, context=None):
4084         act_obj = self.pool.get('ir.actions.act_window')
4085         mod_obj = self.pool.get('ir.model.data')
4086         proc_ids = self.pool.get('procurement.order').search(cr, uid, [('orderpoint_id', 'in', ids), ('state', 'not in', ('done', 'cancel'))], context=context)
4087         result = mod_obj.get_object_reference(cr, uid, 'procurement', 'do_view_procurements')
4088         if not result:
4089             return False
4090
4091         result = act_obj.read(cr, uid, [result[1]], context=context)[0]
4092         result['domain'] = "[('id', 'in', [" + ','.join(map(str, proc_ids)) + "])]"
4093         return result
4094
4095     _columns = {
4096         'name': fields.char('Name', required=True, copy=False),
4097         'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the orderpoint without removing it."),
4098         'logic': fields.selection([('max', 'Order to Max'), ('price', 'Best price (not yet active!)')], 'Reordering Mode', required=True),
4099         'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', required=True, ondelete="cascade"),
4100         'location_id': fields.many2one('stock.location', 'Location', required=True, ondelete="cascade"),
4101         'product_id': fields.many2one('product.product', 'Product', required=True, ondelete='cascade', domain=[('type', '=', 'product')]),
4102         'product_uom': fields.related('product_id', 'uom_id', type='many2one', relation='product.uom', string='Product Unit of Measure', readonly=True, required=True),
4103         'product_min_qty': fields.float('Minimum Quantity', required=True,
4104             digits_compute=dp.get_precision('Product Unit of Measure'),
4105             help="When the virtual stock goes below the Min Quantity specified for this field, Odoo generates "\
4106             "a procurement to bring the forecasted quantity to the Max Quantity."),
4107         'product_max_qty': fields.float('Maximum Quantity', required=True,
4108             digits_compute=dp.get_precision('Product Unit of Measure'),
4109             help="When the virtual stock goes below the Min Quantity, Odoo generates "\
4110             "a procurement to bring the forecasted quantity to the Quantity specified as Max Quantity."),
4111         'qty_multiple': fields.float('Qty Multiple', required=True,
4112             digits_compute=dp.get_precision('Product Unit of Measure'),
4113             help="The procurement quantity will be rounded up to this multiple.  If it is 0, the exact quantity will be used.  "),
4114         'procurement_ids': fields.one2many('procurement.order', 'orderpoint_id', 'Created Procurements'),
4115         'group_id': fields.many2one('procurement.group', 'Procurement Group', help="Moves created through this orderpoint will be put in this procurement group. If none is given, the moves generated by procurement rules will be grouped into one big picking.", copy=False),
4116         'company_id': fields.many2one('res.company', 'Company', required=True),
4117     }
4118     _defaults = {
4119         'active': lambda *a: 1,
4120         'logic': lambda *a: 'max',
4121         'qty_multiple': lambda *a: 1,
4122         'name': lambda self, cr, uid, context: self.pool.get('ir.sequence').get(cr, uid, 'stock.orderpoint') or '',
4123         'product_uom': lambda self, cr, uid, context: context.get('product_uom', False),
4124         'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.warehouse.orderpoint', context=context)
4125     }
4126     _sql_constraints = [
4127         ('qty_multiple_check', 'CHECK( qty_multiple >= 0 )', 'Qty Multiple must be greater than or equal to zero.'),
4128     ]
4129     _constraints = [
4130         (_check_product_uom, 'You have to select a product unit of measure in the same category than the default unit of measure of the product', ['product_id', 'product_uom']),
4131     ]
4132
4133     def default_get(self, cr, uid, fields, context=None):
4134         warehouse_obj = self.pool.get('stock.warehouse')
4135         res = super(stock_warehouse_orderpoint, self).default_get(cr, uid, fields, context)
4136         # default 'warehouse_id' and 'location_id'
4137         if 'warehouse_id' not in res:
4138             warehouse_ids = res.get('company_id') and warehouse_obj.search(cr, uid, [('company_id', '=', res['company_id'])], limit=1, context=context) or []
4139             res['warehouse_id'] = warehouse_ids and warehouse_ids[0] or False
4140         if 'location_id' not in res:
4141             res['location_id'] = res.get('warehouse_id') and warehouse_obj.browse(cr, uid, res['warehouse_id'], context).lot_stock_id.id or False
4142         return res
4143
4144     def onchange_warehouse_id(self, cr, uid, ids, warehouse_id, context=None):
4145         """ Finds location id for changed warehouse.
4146         @param warehouse_id: Changed id of warehouse.
4147         @return: Dictionary of values.
4148         """
4149         if warehouse_id:
4150             w = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context)
4151             v = {'location_id': w.lot_stock_id.id}
4152             return {'value': v}
4153         return {}
4154
4155     def onchange_product_id(self, cr, uid, ids, product_id, context=None):
4156         """ Finds UoM for changed product.
4157         @param product_id: Changed id of product.
4158         @return: Dictionary of values.
4159         """
4160         if product_id:
4161             prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
4162             d = {'product_uom': [('category_id', '=', prod.uom_id.category_id.id)]}
4163             v = {'product_uom': prod.uom_id.id}
4164             return {'value': v, 'domain': d}
4165         return {'domain': {'product_uom': []}}
4166
4167 class stock_picking_type(osv.osv):
4168     _name = "stock.picking.type"
4169     _description = "The picking type determines the picking view"
4170     _order = 'sequence'
4171
4172     def open_barcode_interface(self, cr, uid, ids, context=None):
4173         final_url = "/barcode/web/#action=stock.ui&picking_type_id=" + str(ids[0]) if len(ids) else '0'
4174         return {'type': 'ir.actions.act_url', 'url': final_url, 'target': 'self'}
4175
4176     def _get_tristate_values(self, cr, uid, ids, field_name, arg, context=None):
4177         picking_obj = self.pool.get('stock.picking')
4178         res = {}
4179         for picking_type_id in ids:
4180             #get last 10 pickings of this type
4181             picking_ids = picking_obj.search(cr, uid, [('picking_type_id', '=', picking_type_id), ('state', '=', 'done')], order='date_done desc', limit=10, context=context)
4182             tristates = []
4183             for picking in picking_obj.browse(cr, uid, picking_ids, context=context):
4184                 if picking.date_done > picking.date:
4185                     tristates.insert(0, {'tooltip': picking.name or '' + ": " + _('Late'), 'value': -1})
4186                 elif picking.backorder_id:
4187                     tristates.insert(0, {'tooltip': picking.name or '' + ": " + _('Backorder exists'), 'value': 0})
4188                 else:
4189                     tristates.insert(0, {'tooltip': picking.name or '' + ": " + _('OK'), 'value': 1})
4190             res[picking_type_id] = json.dumps(tristates)
4191         return res
4192
4193     def _get_picking_count(self, cr, uid, ids, field_names, arg, context=None):
4194         obj = self.pool.get('stock.picking')
4195         domains = {
4196             'count_picking_draft': [('state', '=', 'draft')],
4197             'count_picking_waiting': [('state', '=', 'confirmed')],
4198             'count_picking_ready': [('state', 'in', ('assigned', 'partially_available'))],
4199             'count_picking': [('state', 'in', ('assigned', 'waiting', 'confirmed', 'partially_available'))],
4200             'count_picking_late': [('min_date', '<', time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)), ('state', 'in', ('assigned', 'waiting', 'confirmed', 'partially_available'))],
4201             'count_picking_backorders': [('backorder_id', '!=', False), ('state', 'in', ('confirmed', 'assigned', 'waiting', 'partially_available'))],
4202         }
4203         result = {}
4204         for field in domains:
4205             data = obj.read_group(cr, uid, domains[field] +
4206                 [('state', 'not in', ('done', 'cancel')), ('picking_type_id', 'in', ids)],
4207                 ['picking_type_id'], ['picking_type_id'], context=context)
4208             count = dict(map(lambda x: (x['picking_type_id'] and x['picking_type_id'][0], x['picking_type_id_count']), data))
4209             for tid in ids:
4210                 result.setdefault(tid, {})[field] = count.get(tid, 0)
4211         for tid in ids:
4212             if result[tid]['count_picking']:
4213                 result[tid]['rate_picking_late'] = result[tid]['count_picking_late'] * 100 / result[tid]['count_picking']
4214                 result[tid]['rate_picking_backorders'] = result[tid]['count_picking_backorders'] * 100 / result[tid]['count_picking']
4215             else:
4216                 result[tid]['rate_picking_late'] = 0
4217                 result[tid]['rate_picking_backorders'] = 0
4218         return result
4219
4220     def onchange_picking_code(self, cr, uid, ids, picking_code=False):
4221         if not picking_code:
4222             return False
4223         
4224         obj_data = self.pool.get('ir.model.data')
4225         stock_loc = obj_data.xmlid_to_res_id(cr, uid, 'stock.stock_location_stock')
4226         
4227         result = {
4228             'default_location_src_id': stock_loc,
4229             'default_location_dest_id': stock_loc,
4230         }
4231         if picking_code == 'incoming':
4232             result['default_location_src_id'] = obj_data.xmlid_to_res_id(cr, uid, 'stock.stock_location_suppliers')
4233         elif picking_code == 'outgoing':
4234             result['default_location_dest_id'] = obj_data.xmlid_to_res_id(cr, uid, 'stock.stock_location_customers')
4235         return {'value': result}
4236
4237     def _get_name(self, cr, uid, ids, field_names, arg, context=None):
4238         return dict(self.name_get(cr, uid, ids, context=context))
4239
4240     def name_get(self, cr, uid, ids, context=None):
4241         """Overides orm name_get method to display 'Warehouse_name: PickingType_name' """
4242         if context is None:
4243             context = {}
4244         if not isinstance(ids, list):
4245             ids = [ids]
4246         res = []
4247         if not ids:
4248             return res
4249         for record in self.browse(cr, uid, ids, context=context):
4250             name = record.name
4251             if record.warehouse_id:
4252                 name = record.warehouse_id.name + ': ' +name
4253             if context.get('special_shortened_wh_name'):
4254                 if record.warehouse_id:
4255                     name = record.warehouse_id.name
4256                 else:
4257                     name = _('Customer') + ' (' + record.name + ')'
4258             res.append((record.id, name))
4259         return res
4260
4261     def _default_warehouse(self, cr, uid, context=None):
4262         user = self.pool.get('res.users').browse(cr, uid, uid, context)
4263         res = self.pool.get('stock.warehouse').search(cr, uid, [('company_id', '=', user.company_id.id)], limit=1, context=context)
4264         return res and res[0] or False
4265
4266     _columns = {
4267         'name': fields.char('Picking Type Name', translate=True, required=True),
4268         'complete_name': fields.function(_get_name, type='char', string='Name'),
4269         'color': fields.integer('Color'),
4270         'sequence': fields.integer('Sequence', help="Used to order the 'All Operations' kanban view"),
4271         'sequence_id': fields.many2one('ir.sequence', 'Reference Sequence', required=True),
4272         'default_location_src_id': fields.many2one('stock.location', 'Default Source Location'),
4273         'default_location_dest_id': fields.many2one('stock.location', 'Default Destination Location'),
4274         'code': fields.selection([('incoming', 'Suppliers'), ('outgoing', 'Customers'), ('internal', 'Internal')], 'Type of Operation', required=True),
4275         'return_picking_type_id': fields.many2one('stock.picking.type', 'Picking Type for Returns'),
4276         'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', ondelete='cascade'),
4277         'active': fields.boolean('Active'),
4278
4279         # Statistics for the kanban view
4280         'last_done_picking': fields.function(_get_tristate_values,
4281             type='char',
4282             string='Last 10 Done Pickings'),
4283
4284         'count_picking_draft': fields.function(_get_picking_count,
4285             type='integer', multi='_get_picking_count'),
4286         'count_picking_ready': fields.function(_get_picking_count,
4287             type='integer', multi='_get_picking_count'),
4288         'count_picking': fields.function(_get_picking_count,
4289             type='integer', multi='_get_picking_count'),
4290         'count_picking_waiting': fields.function(_get_picking_count,
4291             type='integer', multi='_get_picking_count'),
4292         'count_picking_late': fields.function(_get_picking_count,
4293             type='integer', multi='_get_picking_count'),
4294         'count_picking_backorders': fields.function(_get_picking_count,
4295             type='integer', multi='_get_picking_count'),
4296
4297         'rate_picking_late': fields.function(_get_picking_count,
4298             type='integer', multi='_get_picking_count'),
4299         'rate_picking_backorders': fields.function(_get_picking_count,
4300             type='integer', multi='_get_picking_count'),
4301
4302     }
4303     _defaults = {
4304         'warehouse_id': _default_warehouse,
4305         'active': True,
4306     }
4307
4308 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: