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