[FIX] stock: fixed onchange in pack operation and uom quantity when using pack operation
[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
25 import time
26
27 from openerp.osv import fields, osv
28 from openerp.tools.translate import _
29 from openerp import tools
30 from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT
31 from openerp import SUPERUSER_ID
32 import openerp.addons.decimal_precision as dp
33 import logging
34 _logger = logging.getLogger(__name__)
35
36
37 #----------------------------------------------------------
38 # Incoterms
39 #----------------------------------------------------------
40 class stock_incoterms(osv.osv):
41     _name = "stock.incoterms"
42     _description = "Incoterms"
43     _columns = {
44         'name': fields.char('Name', size=64, 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."),
45         'code': fields.char('Code', size=3, required=True, help="Incoterm Standard Code"),
46         'active': fields.boolean('Active', help="By unchecking the active field, you may hide an INCOTERM you will not use."),
47     }
48     _defaults = {
49         'active': True,
50     }
51
52 #----------------------------------------------------------
53 # Stock Location
54 #----------------------------------------------------------
55
56 class stock_location(osv.osv):
57     _name = "stock.location"
58     _description = "Inventory Locations"
59     _parent_name = "location_id"
60     _parent_store = True
61     _parent_order = 'name'
62     _order = 'parent_left'
63     _rec_name = 'complete_name'
64
65     def _complete_name(self, cr, uid, ids, name, args, context=None):
66         """ Forms complete name of location from parent location to child location.
67         @return: Dictionary of values
68         """
69         res = {}
70         for m in self.browse(cr, uid, ids, context=context):
71             res[m.id] = m.name
72             parent = m.location_id
73             while parent:
74                 res[m.id] = parent.name + ' / ' + res[m.id]
75                 parent = parent.location_id
76         return res
77
78     def _get_sublocations(self, cr, uid, ids, context=None):
79         """ return all sublocations of the given stock locations (included) """
80         if context is None:
81             context = {}
82         context_with_inactive = context.copy()
83         context_with_inactive['active_test']=False
84         return self.search(cr, uid, [('id', 'child_of', ids)], context=context_with_inactive)
85
86     _columns = {
87         'name': fields.char('Location Name', size=64, required=True, translate=True),
88         'active': fields.boolean('Active', help="By unchecking the active field, you may hide a location without deleting it."),
89         'usage': fields.selection([('supplier', 'Supplier Location'), ('view', 'View'), ('internal', 'Internal Location'), ('customer', 'Customer Location'), ('inventory', 'Inventory'), ('procurement', 'Procurement'), ('production', 'Production'), ('transit', 'Transit Location for Inter-Companies Transfers')], 'Location Type', required=True,
90                  help="""* Supplier Location: Virtual location representing the source location for products coming from your suppliers
91                        \n* View: Virtual location used to create a hierarchical structures for your warehouse, aggregating its child locations ; can't directly contain products
92                        \n* Internal Location: Physical locations inside your own warehouses,
93                        \n* Customer Location: Virtual location representing the destination location for products sent to your customers
94                        \n* Inventory: Virtual location serving as counterpart for inventory operations used to correct stock levels (Physical inventories)
95                        \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.
96                        \n* Production: Virtual counterpart location for production operations: this location consumes the raw material and produces finished products
97                       """, select = True),
98
99         'complete_name': fields.function(_complete_name, type='char', string="Location Name",
100                             store={'stock.location': (_get_sublocations, ['name', 'location_id', 'active'], 10)}),
101         'location_id': fields.many2one('stock.location', 'Parent Location', select=True, ondelete='cascade'),
102         'child_ids': fields.one2many('stock.location', 'location_id', 'Contains'),
103
104         'partner_id': fields.many2one('res.partner', 'Owner', help="Owner of the location if not internal"),
105
106         'comment': fields.text('Additional Information'),
107         'posx': fields.integer('Corridor (X)', help="Optional localization details, for information purpose only"),
108         'posy': fields.integer('Shelves (Y)', help="Optional localization details, for information purpose only"),
109         'posz': fields.integer('Height (Z)', help="Optional localization details, for information purpose only"),
110
111         'parent_left': fields.integer('Left Parent', select=1),
112         'parent_right': fields.integer('Right Parent', select=1),
113
114         'company_id': fields.many2one('res.company', 'Company', select=1, help='Let this field empty if this location is shared between all companies'),
115         'scrap_location': fields.boolean('Scrap Location', help='Check this box to allow using this location to put scrapped/damaged goods.'),
116         'removal_strategy_ids': fields.one2many('product.removal', 'location_id', 'Removal Strategies'),
117         'putaway_strategy_ids': fields.one2many('product.putaway', 'location_id', 'Put Away Strategies'),
118     }
119     _defaults = {
120         'active': True,
121         'usage': 'internal',
122         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.location', context=c),
123         'posx': 0,
124         'posy': 0,
125         'posz': 0,
126         'scrap_location': False,
127     }
128     
129     def get_putaway_strategy(self, cr, uid, location, product, context=None):
130         pa = self.pool.get('product.putaway')
131         categ = product.categ_id
132         categs = [categ.id, False]
133         while categ.parent_id:
134             categ = categ.parent_id
135             categs.append(categ.id)
136
137         result = pa.search(cr,uid, [
138             ('location_id', '=', location.id),
139             ('product_categ_id', 'in', categs)
140         ], context=context)
141         if result:
142             return pa.browse(cr, uid, result[0], context=context)
143    
144     def get_removal_strategy(self, cr, uid, location, product, context=None):
145         pr = self.pool.get('product.removal')
146         categ = product.categ_id
147         categs = [categ.id, False]
148         while categ.parent_id:
149             categ = categ.parent_id
150             categs.append(categ.id)
151
152         result = pr.search(cr,uid, [
153             ('location_id', '=', location.id),
154             ('product_categ_id', 'in', categs)
155         ], context=context)
156         if result:
157             return pr.browse(cr, uid, result[0], context=context).method
158
159
160 #----------------------------------------------------------
161 # Routes
162 #----------------------------------------------------------
163
164 class stock_location_route(osv.osv):
165     _name = 'stock.location.route'
166     _description = "Inventory Routes"
167     _order = 'sequence'
168
169     _columns = {
170         'name': fields.char('Route Name', required=True),
171         'sequence': fields.integer('Sequence'),
172         'pull_ids': fields.one2many('procurement.rule', 'route_id', 'Pull Rules'),
173         'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the route without removing it."),
174         'push_ids': fields.one2many('stock.location.path', 'route_id', 'Push Rules'),
175         'product_selectable': fields.boolean('Applicable on Product'),
176         'product_categ_selectable': fields.boolean('Applicable on Product Category'),
177         'warehouse_selectable': fields.boolean('Applicable on Warehouse'),
178         'supplied_wh_id': fields.many2one('stock.warehouse', 'Supplied Warehouse'),
179         'supplier_wh_id': fields.many2one('stock.warehouse', 'Supplier Warehouse'),
180     }
181
182     _defaults = {
183         'sequence': lambda self, cr, uid, ctx: 0,
184         'active': True,
185         'product_selectable': True,
186     }
187
188
189
190 #----------------------------------------------------------
191 # Quants
192 #----------------------------------------------------------
193
194 class stock_quant(osv.osv):
195     """
196     Quants are the smallest unit of stock physical instances
197     """
198     _name = "stock.quant"
199     _description = "Quants"
200
201     def _get_quant_name(self, cr, uid, ids, name, args, context=None):
202         """ Forms complete name of location from parent location to child location.
203         @return: Dictionary of values
204         """
205         res = {}
206         for q in self.browse(cr, uid, ids, context=context):
207
208             res[q.id] = q.product_id.code or ''
209             if q.lot_id:
210                 res[q.id] = q.lot_id.name 
211             res[q.id] += ': '+  str(q.qty) + q.product_id.uom_id.name
212         return res
213
214     _columns = {
215         'name': fields.function(_get_quant_name, type='char', string='Identifier'),
216         'product_id': fields.many2one('product.product', 'Product', required=True),
217         'location_id': fields.many2one('stock.location', 'Location', required=True),
218         'qty': fields.float('Quantity', required=True, help="Quantity of products in this quant, in the default unit of measure of the product"),
219         'package_id': fields.many2one('stock.quant.package', string='Package', help="The package containing this quant"),
220         'packaging_type_id': fields.related('package_id', 'packaging_id', type='many2one', relation='product.packaging', string='Type of packaging', store=True),
221         'reservation_id': fields.many2one('stock.move', 'Reserved for Move', help="The move the quant is reserved for"),
222         'reservation_op_id': fields.many2one('stock.pack.operation', 'Reserved for Pack Operation', help="The operation the quant is reserved for"),
223         'lot_id': fields.many2one('stock.production.lot', 'Lot'),
224         'cost': fields.float('Unit Cost'),
225         'owner_id': fields.many2one('res.partner', 'Owner', help="This is the owner of the quant"),
226
227         'create_date': fields.datetime('Creation Date'),
228         'in_date': fields.datetime('Incoming Date'),
229
230         'history_ids': fields.many2many('stock.move', 'stock_quant_move_rel', 'quant_id', 'move_id', 'Moves', help='Moves that operate(d) on this quant'),
231         'company_id': fields.many2one('res.company', 'Company', help="The company to which the quants belong", required=True),
232
233         # Used for negative quants to reconcile after compensated by a new positive one
234         'propagated_from_id': fields.many2one('stock.quant', 'Linked Quant', help='The negative quant this is coming from'),
235         'negative_dest_location_id': fields.many2one('stock.location', 'Destination Location', help='Technical field used to record the destination location of a move that created a negative quant'),
236     }
237
238     _defaults = {
239         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.quant', context=c),
240     }
241
242     def action_view_quant_history(self, cr, uid, ids, context=None):
243         '''
244         This function returns an action that display the history of the quant, which
245         mean all the stock moves that lead to this quant creation with this quant quantity.
246         '''
247         mod_obj = self.pool.get('ir.model.data')
248         act_obj = self.pool.get('ir.actions.act_window')
249
250         result = mod_obj.get_object_reference(cr, uid, 'stock', 'action_move_form2')
251         id = result and result[1] or False
252         result = act_obj.read(cr, uid, [id], context={})[0]
253
254         move_ids = []
255         for quant in self.browse(cr, uid, ids, context=context):
256             move_ids += [move.id for move in quant.history_ids]
257             
258         result['domain'] = "[('id','in',[" + ','.join(map(str, move_ids)) + "])]"
259         return result
260
261     def quants_reserve(self, cr, uid, quants, move, context=None):
262         toreserve = []
263         for quant,qty in quants:
264             if not quant: continue
265             self._quant_split(cr, uid, quant, qty, context=context)
266             toreserve.append(quant.id)
267         return self.write(cr, SUPERUSER_ID, toreserve, {'reservation_id': move.id}, context=context)
268
269     # add location_dest_id in parameters (False=use the destination of the move)
270     def quants_move(self, cr, uid, quants, move, lot_id = False, owner_id = False, package_id = False, context=None):
271         for quant, qty in quants:
272             #quant may be a browse record or None
273             quant_record = self.move_single_quant(cr, uid, quant, qty, move, lot_id = lot_id, package_id = package_id, context=context)
274             #quant_record is the quant newly created or already split
275             self._quant_reconcile_negative(cr, uid, quant_record, context=context)
276
277
278     def check_preferred_location(self, cr, uid, move, context=None):
279         if move.putaway_ids and move.putaway_ids[0]:
280             #Take only first suggestion for the moment
281             return move.putaway_ids[0].location_id
282         return move.location_dest_id
283
284     def move_single_quant(self, cr, uid, quant, qty, move, lot_id = False, owner_id = False, package_id = False, context=None):
285         if not quant:
286             quant = self._quant_create(cr, uid, qty, move, lot_id = lot_id, owner_id = owner_id, package_id = package_id, context = context)
287         else:
288             self._quant_split(cr, uid, quant, qty, context=context)
289         # FP Note: improve this using preferred locations
290         location_to = self.check_preferred_location(cr, uid, move, context=context)
291         self.write(cr, SUPERUSER_ID, [quant.id], {
292             'location_id': location_to.id,
293             'history_ids': [(4, move.id)]
294         })
295         quant.refresh()
296         return quant
297
298     def quants_get_prefered_domain(self, cr, uid, location, product, qty, domain=None, prefered_domain=False, fallback_domain=False, restrict_lot_id=False, restrict_partner_id=False, context=None):
299         ''' This function tries to find quants in the given location for the given domain, by trying to first limit
300             the choice on the quants that match the prefered_domain as well. But if the qty requested is not reached
301             it tries to find the remaining quantity by using the fallback_domain.
302         '''
303         if prefered_domain and fallback_domain:
304             if domain is None:
305                 domain = []
306             quants = self.quants_get(cr, uid, location, product, qty, domain=domain + prefered_domain, restrict_lot_id=restrict_lot_id, restrict_partner_id=restrict_partner_id, context=context)
307             res_qty = qty
308             quant_ids = []
309             for quant in quants:
310                 if quant[0]:
311                     quant_ids.append(quant[0].id)
312                     res_qty -= quant[1]
313             if res_qty > 0:
314                 #try to replace the last tuple (None, res_qty) with something that wasn't chosen at first because of the prefered order
315                 quants.pop()
316                 #make sure the quants aren't found twice (if the prefered_domain and the fallback_domain aren't orthogonal
317                 domain += [('id', 'not in', quant_ids)]
318                 unprefered_quants = self.quants_get(cr, uid, location, product, res_qty, domain=domain + fallback_domain, restrict_lot_id=restrict_lot_id, restrict_partner_id=restrict_partner_id, context=context)
319                 for quant in unprefered_quants:
320                     quants.append(quant)
321             return quants
322         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)
323
324     def quants_get(self, cr, uid, location, product, qty, domain=None, restrict_lot_id=False, restrict_partner_id=False, context=None):
325         """
326         Use the removal strategies of product to search for the correct quants
327         If you inherit, put the super at the end of your method.
328
329         :location: browse record of the parent location in which the quants have to be found
330         :product: browse record of the product to find
331         :qty in UoM of product
332         """
333         result = []
334         domain = domain or [('qty', '>', 0.0)]
335         if restrict_partner_id:
336             domain += [('owner_id', '=', restrict_partner_id)]
337         if restrict_lot_id:
338             domain += [('lot_id', '=', restrict_lot_id)]
339         if location:
340             removal_strategy = self.pool.get('stock.location').get_removal_strategy(cr, uid, location, product, context=context) or 'fifo'
341             if removal_strategy == 'fifo':
342                 result += self._quants_get_fifo(cr, uid, location, product, qty, domain, context=context)
343             elif removal_strategy == 'lifo':
344                 result += self._quants_get_lifo(cr, uid, location, product, qty, domain, context=context)
345             else:
346                 raise osv.except_osv(_('Error!'), _('Removal strategy %s not implemented.' % (removal_strategy,)))
347         return result
348
349     def _quant_create(self, cr, uid, qty, move, lot_id=False, owner_id=False, package_id = False, force_location=False, context=None):
350         '''Create a quant in the destination location and create a negative quant in the source location if it's an internal location.
351         '''
352         if context is None:
353             context = {}
354         price_unit = self.pool.get('stock.move').get_price_unit(cr, uid, move, context=context)
355         location = force_location or move.location_dest_id
356         vals = {
357             'product_id': move.product_id.id,
358             'location_id': location.id,
359             'qty': qty,
360             'cost': price_unit,
361             'history_ids': [(4, move.id)],
362             'in_date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
363             'company_id': move.company_id.id,
364             'lot_id': lot_id,
365             'owner_id': owner_id,
366         }
367
368         if move.location_id.usage == 'internal':
369             #if we were trying to move something from an internal location and reach here (quant creation),
370             #it means that a negative quant has to be created as well.
371             negative_vals = vals.copy()
372             negative_vals['location_id'] = move.location_id.id
373             negative_vals['qty'] = -qty
374             negative_vals['cost'] = price_unit
375             negative_vals['negative_dest_location_id'] = move.location_dest_id.id
376             negative_vals['package_id'] = package_id
377             negative_quant_id = self.create(cr, SUPERUSER_ID, negative_vals, context=context)
378             vals.update({'propagated_from_id': negative_quant_id})
379
380         #create the quant as superuser, because we want to restrict the creation of quant manually: they should always use this method to create quants
381         quant_id = self.create(cr, SUPERUSER_ID, vals, context=context)
382         return self.browse(cr, uid, quant_id, context=context)
383
384     def _quant_split(self, cr, uid, quant, qty, context=None):
385         context = context or {}
386         if (quant.qty > 0 and quant.qty <= qty) or (quant.qty <= 0 and quant.qty >= qty):
387             return False
388         new_quant = self.copy(cr, SUPERUSER_ID, quant.id, default={'qty': quant.qty - qty}, context=context)
389         self.write(cr, SUPERUSER_ID, quant.id, {'qty': qty}, context=context)
390         quant.refresh()
391         return self.browse(cr, uid, new_quant, context=context)
392
393     def _get_latest_move(self, cr, uid, quant, context=None):
394         move = False
395         for m in quant.history_ids:
396             if not move or m.date > move.date:
397                 move = m
398         return move
399
400     #def _reconcile_single_negative_quant(self, cr, uid, to_solve_quant, quant, quant_neg, qty, context=None):
401     #    move = self._get_latest_move(cr, uid, to_solve_quant, context=context)
402     #    remaining_solving_quant = self._quant_split(cr, uid, quant, qty, context=context)
403     #    remaining_to_solve_quant = self._quant_split(cr, uid, to_solve_quant, qty, context=context)
404     #    remaining_neg_quant = self._quant_split(cr, uid, quant_neg, -qty, context=context)
405     #    #if the reconciliation was not complete, we need to link together the remaining parts
406     #    if remaining_to_solve_quant and remaining_neg_quant:
407     #        self.write(cr, uid, remaining_to_solve_quant.id, {'propagated_from_id': remaining_neg_quant.id}, context=context)
408     #    #delete the reconciled quants, as it is replaced by the solving quant
409     #    if remaining_neg_quant:
410     #        otherquant_ids = self.search(cr, uid, [('propagated_from_id', '=', quant_neg.id)], context=context)
411     #        self.write(cr, uid, otherquant_ids, {'propagated_from_id': remaining_neg_quant.id}, context=context)
412     #    self.unlink(cr, SUPERUSER_ID, [quant_neg.id, to_solve_quant.id], context=context)
413     #    #call move_single_quant to ensure recursivity if necessary and do the stock valuation
414     #    self.move_single_quant(cr, uid, quant, qty, move, context=context)
415     #    return remaining_solving_quant, remaining_to_solve_quant
416
417     def _quants_merge(self, cr, uid, solved_quant_ids, solving_quant, context=None):
418         path = []
419         for move in solving_quant.history_ids:
420             path.append((4, move.id))
421         self.write(cr, SUPERUSER_ID, solved_quant_ids, {'history_ids': path}, context=context)
422
423     def _quant_reconcile_negative(self, cr, uid, quant, context=None):
424         """
425             When new quant arrive in a location, try to reconcile it with
426             negative quants. If it's possible, apply the cost of the new
427             quant to the conter-part of the negative quant.
428         """
429         if quant.location_id.usage != 'internal':
430             return False
431         solving_quant = quant
432         dom = [('qty', '<', 0)]
433         dom += [('lot_id', '=', quant.lot_id and quant.lot_id.id or False)]
434         dom += [('owner_id', '=', quant.owner_id and quant.owner_id.id or False)]
435         dom += [('package_id', '=', quant.package_id and quant.package_id.id or False)]
436         quants = self.quants_get(cr, uid, quant.location_id, quant.product_id, quant.qty, [('qty', '<', '0')], context=context)
437         for quant_neg, qty in quants:
438             if not quant_neg:
439                 continue
440             to_solve_quant_ids = self.search(cr, uid, [('propagated_from_id', '=', quant_neg.id)], context=context)
441             if not to_solve_quant_ids:
442                 continue
443             solving_qty = qty
444             solved_quant_ids = []
445             for to_solve_quant in self.browse(cr, uid, to_solve_quant_ids, context=context):
446                 if solving_qty <= 0:
447                     continue
448                 solved_quant_ids.append(to_solve_quant.id)
449                 self._quant_split(cr, uid, to_solve_quant, min(solving_qty, to_solve_quant.qty), context=context)
450                 solving_qty -= min(solving_qty, to_solve_quant.qty)
451             remaining_solving_quant = self._quant_split(cr, uid, solving_quant, qty, context=context)
452             remaining_neg_quant = self._quant_split(cr, uid, quant_neg, -qty, context=context)
453             #if the reconciliation was not complete, we need to link together the remaining parts
454             if remaining_neg_quant:
455                 remaining_to_solve_quant_ids = self.search(cr, uid, [('propagated_from_id', '=', quant_neg.id), ('id', 'not in', solved_quant_ids)], context=context)
456                 if remaining_to_solve_quant_ids:
457                     self.write(cr, SUPERUSER_ID, remaining_to_solve_quant_ids, {'propagated_from_id': remaining_neg_quant.id}, context=context)
458             #delete the reconciled quants, as it is replaced by the solved quants
459             self.unlink(cr, SUPERUSER_ID, [quant_neg.id], context=context)
460             #price update + accounting entries adjustments
461             self._price_update(cr, uid, solved_quant_ids, solving_quant.cost, context=context)
462             #merge history (and cost?)
463             self._quants_merge(cr, uid, solved_quant_ids, solving_quant, context=context)
464             self.unlink(cr, SUPERUSER_ID, [solving_quant.id], context=context)
465             solving_quant = remaining_solving_quant
466
467             #solving_quant, dummy = self._reconcile_single_negative_quant(cr, uid, to_solve_quant, solving_quant, quant_neg, qty, context=context)
468
469     def _price_update(self, cr, uid, ids, newprice, context=None):
470         self.write(cr, SUPERUSER_ID, ids, {'cost': newprice}, context=context)
471
472     def write(self, cr, uid, ids, vals, context=None):
473         #We want to trigger the move with nothing on reserved_quant_ids for the store of the remaining quantity
474         if 'reservation_id' in vals:
475             reservation_ids = self.browse(cr, uid, ids, context=context)
476             moves_to_warn = set()
477             for reser in reservation_ids:
478                 if reser.reservation_id:
479                     moves_to_warn.add(reser.reservation_id.id)
480             self.pool.get('stock.move').write(cr, uid, list(moves_to_warn), {'reserved_quant_ids': []}, context=context)
481         return super(stock_quant, self).write(cr, SUPERUSER_ID, ids, vals, context=context)
482
483     def quants_unreserve(self, cr, uid, move, context=None):
484         related_quants = [x.id for x in move.reserved_quant_ids]
485         return self.write(cr, SUPERUSER_ID, related_quants, {'reservation_id': False, 'reservation_op_id': False}, context=context)
486
487     def _quants_get_order(self, cr, uid, location, product, quantity, domain=[], orderby='in_date', context=None):
488         ''' Implementation of removal strategies
489             If it can not reserve, it will return a tuple (None, qty)
490         '''
491         domain += location and [('location_id', 'child_of', location.id)] or []
492         domain += [('product_id', '=', product.id)] + domain
493         res = []
494         offset = 0
495         while quantity > 0:
496             quants = self.search(cr, uid, domain, order=orderby, limit=10, offset=offset, context=context)
497             if not quants:
498                 res.append((None, quantity))
499                 break
500             for quant in self.browse(cr, uid, quants, context=context):
501                 if quantity >= abs(quant.qty):
502                     res += [(quant, abs(quant.qty))]
503                     quantity -= abs(quant.qty)
504                 elif quantity != 0:
505                     res += [(quant, quantity)]
506                     quantity = 0
507                     break
508             offset += 10
509         return res
510
511     def _quants_get_fifo(self, cr, uid, location, product, quantity, domain=[], context=None):
512         order = 'in_date'
513         return self._quants_get_order(cr, uid, location, product, quantity, domain, order, context=context)
514
515     def _quants_get_lifo(self, cr, uid, location, product, quantity, domain=[], context=None):
516         order = 'in_date desc'
517         return self._quants_get_order(cr, uid, location, product, quantity, domain, order, context=context)
518
519     def _location_owner(self, cr, uid, quant, location, context=None):
520         ''' Return the company owning the location if any '''
521         return location and (location.usage == 'internal') and location.company_id or False
522
523     def _check_location(self, cr, uid, ids, context=None):
524         for record in self.browse(cr, uid, ids, context=context):
525             if record.location_id.usage == 'view':
526                 raise osv.except_osv(_('Error'), _('You cannot move product %s to a location of type view %s.')% (record.product_id.name, record.location_id.name))
527         return True
528
529     # FP Note: rehab this, with the auto creation algo
530     # def _check_tracking(self, cr, uid, ids, context=None):
531     #     """ Checks if serial number is assigned to stock move or not.
532     #     @return: True or False
533     #     """
534     #     for move in self.browse(cr, uid, ids, context=context):
535     #         if not move.lot_id and \
536     #            (move.state == 'done' and \
537     #            ( \
538     #                (move.product_id.track_production and move.location_id.usage == 'production') or \
539     #                (move.product_id.track_production and move.location_dest_id.usage == 'production') or \
540     #                (move.product_id.track_incoming and move.location_id.usage == 'supplier') or \
541     #                (move.product_id.track_outgoing and move.location_dest_id.usage == 'customer') or \
542     #                (move.product_id.track_incoming and move.location_id.usage == 'inventory') \
543     #            )):
544     #             return False
545     #     return True
546
547     _constraints = [
548         (_check_location, 'You cannot move products to a location of the type view.', ['location_id'])
549     #    (_check_tracking, 'You must assign a serial number for this product.', ['prodlot_id']),
550     ]
551
552
553 #----------------------------------------------------------
554 # Stock Picking
555 #----------------------------------------------------------
556
557 class stock_picking(osv.osv):
558     _name = "stock.picking"
559     _inherit = ['mail.thread']
560     _description = "Picking List"
561     _order = "priority desc, date desc, id desc"
562
563     def _set_min_date(self, cr, uid, id, field, value, arg, context=None):
564         move_obj = self.pool.get("stock.move")
565         if value:
566             move_ids = [move.id for move in self.browse(cr, uid, id, context=context).move_lines]
567             move_obj.write(cr, uid, move_ids, {'date_expected': value}, context=context)
568
569     def get_min_max_date(self, cr, uid, ids, field_name, arg, context=None):
570         """ Finds minimum and maximum dates for picking.
571         @return: Dictionary of values
572         """
573         res = {}
574         for id in ids:
575             res[id] = {'min_date': False, 'max_date': False}
576         if not ids:
577             return res
578         cr.execute("""select
579                 picking_id,
580                 min(date_expected),
581                 max(date_expected)
582             from
583                 stock_move
584             where
585                 picking_id IN %s
586             group by
587                 picking_id""",(tuple(ids),))
588         for pick, dt1, dt2 in cr.fetchall():
589             res[pick]['min_date'] = dt1
590             res[pick]['max_date'] = dt2
591         return res
592
593     def create(self, cr, user, vals, context=None):
594         context = context or {}
595         if ('name' not in vals) or (vals.get('name') in ('/', False)):
596             ptype_id = vals.get('picking_type_id', context.get('default_picking_type_id', False))
597             sequence_id = self.pool.get('stock.picking.type').browse(cr, user, ptype_id, context=context).sequence_id.id
598             vals['name'] = self.pool.get('ir.sequence').get_id(cr, user, sequence_id, 'id', context=context)
599                 
600         return super(stock_picking, self).create(cr, user, vals, context)
601
602     def _state_get(self, cr, uid, ids, field_name, arg, context=None):
603         '''The state of a picking depends on the state of its related stock.move
604             draft: the picking has no line or any one of the lines is draft
605             done, draft, cancel: all lines are done / draft / cancel
606             confirmed, auto, assigned depends on move_type (all at once or direct)
607         '''
608         res = {}
609         for pick in self.browse(cr, uid, ids, context=context):
610             if (not pick.move_lines) or any([x.state == 'draft' for x in pick.move_lines]):
611                 res[pick.id] = 'draft'
612                 continue
613             if all([x.state == 'cancel' for x in pick.move_lines]):
614                 res[pick.id] = 'cancel'
615                 continue
616             if all([x.state in ('cancel','done') for x in pick.move_lines]):
617                 res[pick.id] = 'done'
618                 continue
619
620             order = {'confirmed':0, 'waiting':1, 'assigned':2}
621             order_inv = dict(zip(order.values(),order.keys()))
622             lst = [order[x.state] for x in pick.move_lines if x.state not in ('cancel','done')]
623             if pick.move_lines == 'one':
624                 res[pick.id] = order_inv[min(lst)]
625             else:
626                 res[pick.id] = order_inv[max(lst)]
627         return res
628
629     def _get_pickings(self, cr, uid, ids, context=None):
630         res = set()
631         for move in self.browse(cr, uid, ids, context=context):
632             if move.picking_id:
633                 res.add(move.picking_id.id)
634         return list(res)
635
636     def _get_pack_operation_exist(self, cr, uid, ids, field_name, arg, context=None):
637         res = {}
638         for pick in self.browse(cr, uid, ids, context=context):
639             res[pick.id] = False
640             if pick.pack_operation_ids:
641                 res[pick.id] = True
642         return res
643
644     def _get_quant_reserved_exist(self, cr, uid, ids, field_name, arg, context=None):
645         res = {}
646         for pick in self.browse(cr, uid, ids, context=context):            
647             res[pick.id] = False
648             for move in pick.move_lines:
649                 if move.reserved_quant_ids:
650                     res[pick.id] = True
651                     continue      
652         return res
653
654     def action_assign_owner(self, cr, uid, ids, context=None):
655         for picking in self.browse(cr, uid, ids, context=context):
656             packop_ids = [op.id for op in picking.pack_operation_ids] 
657             self.pool.get('stock.pack.operation').write(cr, uid, packop_ids, {'owner_id': picking.owner_id.id}, context=context)
658
659     _columns = {
660         'name': fields.char('Reference', size=64, select=True, states={'done':[('readonly', True)], 'cancel':[('readonly',True)]}),
661         'origin': fields.char('Source Document', size=64, states={'done':[('readonly', True)], 'cancel':[('readonly',True)]}, help="Reference of the document", select=True),
662         '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),
663         'note': fields.text('Notes', states={'done':[('readonly', True)], 'cancel':[('readonly',True)]}),
664         '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"),
665         'state': fields.function(_state_get, type="selection", store = {
666             'stock.picking': (lambda self, cr, uid, ids, ctx: ids, ['move_type', 'move_lines'], 20),
667             'stock.move': (_get_pickings, ['state', 'picking_id'], 20)}, selection = [
668             ('draft', 'Draft'),
669             ('cancel', 'Cancelled'),
670             ('waiting', 'Waiting Another Operation'),
671             ('confirmed', 'Waiting Availability'),
672             ('assigned', 'Ready to Transfer'),
673             ('done', 'Transferred'),
674             ], string='Status', readonly=True, select=True, track_visibility='onchange', help="""
675             * Draft: not confirmed yet and will not be scheduled until confirmed\n
676             * Waiting Another Operation: waiting for another move to proceed before it becomes automatically available (e.g. in Make-To-Order flows)\n
677             * Waiting Availability: still waiting for the availability of products\n
678             * Ready to Transfer: products reserved, simply waiting for confirmation.\n
679             * Transferred: has been processed, can't be modified or cancelled anymore\n
680             * Cancelled: has been cancelled, can't be confirmed anymore"""
681         ),
682         'priority': fields.selection([('0', 'Low'), ('1', 'Normal'), ('2', 'High')], string='Priority', required=True),
683         'min_date': fields.function(get_min_max_date, multi="min_max_date", fnct_inv=_set_min_date,
684                  store={'stock.move': (_get_pickings, ['state', 'date_expected'], 20)}, type='datetime', string='Scheduled Date', select=1, help="Scheduled time for the first part of the shipment to be processed"),
685         'max_date': fields.function(get_min_max_date, multi="min_max_date",
686                  store={'stock.move': (_get_pickings, ['state', 'date_expected'], 20)}, type='datetime', string='Max. Expected Date', select=2, help="Scheduled time for the last part of the shipment to be processed"),
687         'date': fields.datetime('Commitment Date', help="Date promised for the completion of the transfer order, usually set the time of the order and revised later on.", select=True, states={'done':[('readonly', True)], 'cancel':[('readonly',True)]}),
688         'date_done': fields.datetime('Date of Transfer', help="Date of Completion", states={'done':[('readonly', True)], 'cancel':[('readonly',True)]}),
689         'move_lines': fields.one2many('stock.move', 'picking_id', 'Internal Moves', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}),
690         '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'),
691         'partner_id': fields.many2one('res.partner', 'Partner', states={'done':[('readonly', True)], 'cancel':[('readonly',True)]}),
692         'company_id': fields.many2one('res.company', 'Company', required=True, select=True, states={'done':[('readonly', True)], 'cancel':[('readonly',True)]}),
693         'pack_operation_ids': fields.one2many('stock.pack.operation', 'picking_id', string='Related Packing Operations'),
694         'pack_operation_exist': fields.function(_get_pack_operation_exist, type='boolean', string='Pack Operation Exists?', help='technical field for attrs in view'),
695         'picking_type_id': fields.many2one('stock.picking.type', 'Picking Type', required=True),
696
697         'owner_id': fields.many2one('res.partner', 'Owner', help="Default Owner"),
698         # Used to search on pickings
699         'product_id': fields.related('move_lines', 'product_id', type='many2one', relation='product.product', string='Product'),#?
700         'location_id': fields.related('move_lines', 'location_id', type='many2one', relation='stock.location', string='Location', readonly=True),
701         'location_dest_id': fields.related('move_lines', 'location_dest_id', type='many2one', relation='stock.location', string='Destination Location', readonly=True),
702         'group_id': fields.related('move_lines', 'group_id', type='many2one', relation='procurement.group', string='Procurement Group', readonly=True, 
703               store={
704                   'stock.picking': (lambda self, cr, uid, ids, ctx: ids, ['move_lines'], 10),
705                   'stock.move': (_get_pickings, ['group_id', 'picking_id'], 10),
706               }),
707     }
708
709     _defaults = {
710         'name': lambda self, cr, uid, context: '/',
711         'state': 'draft',
712         'move_type': 'one',
713         'priority' : '1', #normal
714         'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
715         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.picking', context=c)
716     }
717     _sql_constraints = [
718         ('name_uniq', 'unique(name, company_id)', 'Reference must be unique per company!'),
719     ]
720
721     def copy(self, cr, uid, id, default=None, context=None):
722         if default is None:
723             default = {}
724         default = default.copy()
725         picking_obj = self.browse(cr, uid, id, context=context)
726         if ('name' not in default) or (picking_obj.name == '/'):
727             default['name'] = '/'
728         if not default.get('backorder_id'):
729             default['backorder_id'] = False
730                     
731         return super(stock_picking, self).copy(cr, uid, id, default, context)
732         
733
734     def action_confirm(self, cr, uid, ids, context=None):
735         todo = []
736         todo_force_assign = []
737         
738         for picking in self.browse(cr, uid, ids, context=context):
739             if picking.picking_type_id.auto_force_assign:
740                 todo_force_assign.append(picking.id)
741             for r in picking.move_lines:
742                 if r.state == 'draft':
743                     todo.append(r.id)
744         if len(todo):
745             self.pool.get('stock.move').action_confirm(cr, uid, todo, context=context)
746
747         if todo_force_assign:
748             self.force_assign(cr, uid, todo_force_assign, context=context)
749         
750         return True
751
752     def action_assign(self, cr, uid, ids, *args):
753         """ Changes state of picking to available if all moves are confirmed.
754         @return: True
755         """
756         for pick in self.browse(cr, uid, ids):
757             if pick.state == 'draft':
758                 self.action_confirm(cr, uid, [pick.id])
759             move_ids = [x.id for x in pick.move_lines if x.state == 'confirmed']
760             if not move_ids:
761                 raise osv.except_osv(_('Warning!'), _('No product available.'))
762             self.pool.get('stock.move').action_assign(cr, uid, move_ids)
763         return True
764
765     def force_assign(self, cr, uid, ids, context=None):
766         """ Changes state of picking to available if moves are confirmed or waiting.
767         @return: True
768         """
769         for pick in self.browse(cr, uid, ids, context=context):
770             move_ids = [x.id for x in pick.move_lines if x.state in ['confirmed', 'waiting']]
771             self.pool.get('stock.move').force_assign(cr, uid, move_ids, context=context)
772         return True
773
774     def cancel_assign(self, cr, uid, ids, *args):
775         """ Cancels picking and moves.
776         @return: True
777         """
778         for pick in self.browse(cr, uid, ids):
779             move_ids = [x.id for x in pick.move_lines]
780             self.pool.get('stock.move').cancel_assign(cr, uid, move_ids)
781         return True
782
783     def action_cancel(self, cr, uid, ids, context=None):
784         for pick in self.browse(cr, uid, ids, context=context):
785             ids2 = [move.id for move in pick.move_lines]
786             self.pool.get('stock.move').action_cancel(cr, uid, ids2, context)
787         return True
788
789     def action_done(self, cr, uid, ids, context=None):
790         """Changes picking state to done by processing the Stock Moves of the Picking
791
792         Normally that happens when the button "Done" is pressed on a Picking view.
793         @return: True
794         """
795         for pick in self.browse(cr, uid, ids, context=context):
796             todo = []
797             for move in pick.move_lines:
798                 if move.state == 'draft':
799                     self.pool.get('stock.move').action_confirm(cr, uid, [move.id],
800                         context=context)
801                     todo.append(move.id)
802                 elif move.state in ('assigned','confirmed'):
803                     todo.append(move.id)
804             if len(todo):
805                 self.pool.get('stock.move').action_done(cr, uid, todo, context=context)
806         return True
807
808     def unlink(self, cr, uid, ids, context=None):
809         move_obj = self.pool.get('stock.move')
810         context = context or {}
811         for pick in self.browse(cr, uid, ids, context=context):
812             ids2 = [move.id for move in pick.move_lines]
813             move_obj.action_cancel(cr, uid, ids2, context=context)
814             move_obj.unlink(cr, uid, ids2, context=context)
815         return super(stock_picking, self).unlink(cr, uid, ids, context=context)
816
817     # Methods for partial pickings
818
819     def _create_backorder(self, cr, uid, picking, backorder_moves=[], context=None):
820         """
821             Move all non-done lines into a new backorder picking
822         """
823         if not backorder_moves:
824             backorder_moves = picking.move_lines
825         backorder_move_ids = [x.id for x in backorder_moves if x.state not in ('done','cancel')]
826         if 'do_only_split' in context and context['do_only_split']:
827             backorder_move_ids = [x.id for x in backorder_moves if x.id not in context['split']]
828
829         if backorder_move_ids:
830             backorder_id = self.copy(cr, uid, picking.id, {
831                 'name': '/',
832                 'move_lines': [],
833                 'pack_operation_ids': [],
834                 'backorder_id': picking.id,
835             })
836             back_order_name = self.browse(cr, uid, backorder_id, context=context).name
837             self.message_post(cr, uid, picking.id, body=_("Back order <em>%s</em> <b>created</b>.") % (back_order_name), context=context)
838             move_obj = self.pool.get("stock.move")
839             move_obj.write(cr, uid, backorder_move_ids, {'picking_id': backorder_id}, context=context)
840             
841             self.pool.get("stock.picking").action_confirm(cr, uid, [picking.id], context=context)
842             self.action_confirm(cr, uid, [backorder_id], context=context)
843             return backorder_id
844         return False
845
846     def do_prepare_partial(self, cr, uid, picking_ids, context=None):
847         context = context or {}
848         pack_operation_obj = self.pool.get('stock.pack.operation')
849         pack_obj = self.pool.get("stock.quant.package")
850         quant_obj = self.pool.get("stock.quant")
851         for picking in self.browse(cr, uid, picking_ids, context=context):
852             for move in picking.move_lines:
853                 if move.state != 'assigned': continue
854                 #Check which of the reserved quants are entirely in packages (can be in separate method)
855                 packages = list(set([x.package_id for x in move.reserved_quant_ids if x.package_id]))
856                 done_packages = []
857                 for pack in packages:
858                     cont = True
859                     good_pack = False
860                     test_pack = pack
861                     while cont:
862                         quants = pack_obj.get_content(cr, uid, [test_pack.id], context=context)
863                         if all([x.reservation_id.id == move.id for x in quant_obj.browse(cr, uid, quants, context=context) if x.reservation_id]):
864                             good_pack = test_pack.id
865                             if test_pack.parent_id:
866                                 test_pack = test_pack.parent_id
867                             else:
868                                 cont = False
869                         else:
870                             cont = False
871                     if good_pack:
872                         done_packages.append(good_pack)
873                 done_packages = list(set(done_packages))
874
875                 #Create package operations
876                 reserved = set([x.id for x in move.reserved_quant_ids])
877                 remaining_qty = move.product_qty
878                 for pack in pack_obj.browse(cr, uid, done_packages, context=context):
879                     quantl = pack_obj.get_content(cr, uid, [pack.id], context=context)
880                     for quant in quant_obj.browse(cr, uid, quantl, context=context):
881                         remaining_qty -= quant.qty
882                     quants = set(pack_obj.get_content(cr, uid, [pack.id], context=context))
883                     reserved -= quants
884                     pack_operation_obj.create(cr, uid, {
885                         'picking_id': picking.id,
886                         'package_id': pack.id,
887                         'product_qty': 1.0,
888                     }, context=context)
889
890                 yet_to_reserve = list(reserved)
891                 #Create operations based on quants
892                 for quant in quant_obj.browse(cr, uid, yet_to_reserve, context=context):
893                     qty = min(quant.qty, move.product_qty)
894                     remaining_qty -= qty
895                     pack_operation_obj.create(cr, uid, {
896                         'picking_id': picking.id,
897                         'product_qty': qty,
898                         'quant_id': quant.id,
899                         'product_id': quant.product_id.id,
900                         'lot_id': quant.lot_id and quant.lot_id.id or False,
901                         'product_uom_id': quant.product_id.uom_id.id,
902                         'owner_id': quant.owner_id and quant.owner_id.id or False,
903                         'cost': quant.cost,
904                         'package_id': quant.package_id and quant.package_id.id or False,
905                     }, context=context)
906                 if remaining_qty > 0:
907                     pack_operation_obj.create(cr, uid, {
908                         'picking_id': picking.id,
909                         'product_qty': remaining_qty,
910                         'product_id': move.product_id.id,
911                         'product_uom_id': move.product_id.uom_id.id,
912                         'cost': move.product_id.standard_price,
913                     }, context=context)
914
915
916     def do_rereserve(self, cr, uid, picking_ids, context=None):
917         '''
918             Needed for parameter create
919         '''
920         self.rereserve(cr, uid, picking_ids, context=context)
921
922     def do_unreserve(self,cr,uid,picking_ids, context=None):
923         """
924           Will remove all quants for picking in picking_ids
925         """
926         ids_to_free = []
927         quant_obj = self.pool.get("stock.quant")
928         for picking in self.browse(cr, uid, picking_ids, context=context):
929             for move in picking.move_lines:
930                 ids_to_free += [quant.id for quant in move.reserved_quant_ids]
931         if ids_to_free:
932             quant_obj.write(cr, SUPERUSER_ID, ids_to_free, {'reservation_id' : False, 'reservation_op_id': False }, context = context)
933
934     def _reserve_quants_ops_move(self, cr, uid, ops, move, qty, create=False, context=None):
935         """
936           Will return the quantity that could not be reserved
937         """
938         quant_obj = self.pool.get("stock.quant")
939         op_obj = self.pool.get("stock.pack.operation")
940         if create and move.location_id.usage != 'internal':
941             # Create quants
942             quant = quant_obj._quant_create(cr, uid, qty, move, lot_id=ops.lot_id and ops.lot_id.id or False, owner_id=ops.owner_id and ops.owner_id.id or False, context=context)
943             quant.write({'reservation_op_id': ops.id, 'location_id': move.location_id.id})
944             quant_obj.quants_reserve(cr, uid, [(quant, qty)], move, context=context)
945             return 0
946         else:
947             #Quants get
948             dom = op_obj._get_domain(cr, uid, ops, context=context)
949             dom = dom + [('reservation_id', 'not in', [x.id for x in move.picking_id.move_lines])]
950             quants = quant_obj.quants_get_prefered_domain(cr, uid, move.location_id, move.product_id, qty, domain=dom, prefered_domain=[('reservation_id', '=', False)], fallback_domain=[('reservation_id', '!=', False)], context=context)
951             res_qty = qty
952             for quant in quants:
953                 if quant[0]:  # If quant can be reserved
954                     res_qty -= quant[1]
955             quant_obj.quants_reserve(cr, uid, quants, move, context=context)
956             quant_obj.write(cr, SUPERUSER_ID, [x[0].id for x in quants if x[0]], {'reservation_op_id': ops.id}, context=context)
957             return res_qty
958
959     def rereserve(self, cr, uid, picking_ids, create=False, context=None):
960         """
961             This will unreserve all products and reserve the quants from the operations again
962             :return: Tuple (res, res2, resneg)
963                 res: dictionary of ops with quantity that could not be processed matching ops and moves
964                 res2: dictionary of moves with quantity that could not be processed matching ops and moves
965                 resneg: the negative quants to be created: resneg[move][ops] gives negative quant to be created
966             tuple of dictionary with quantities of quant operation and product that can not be matched between ops and moves
967             and dictionary with remaining values on moves
968         """
969         quant_obj = self.pool.get("stock.quant")
970         pack_obj = self.pool.get("stock.quant.package")
971         uom_obj = self.pool.get('product.uom')
972         res = {} # Qty still to do from ops
973         res2 = {} #what is left from moves
974         resneg= {} #Number of negative quants to create for move/op
975         for picking in self.browse(cr, uid, picking_ids, context=context):
976             products_moves = {}
977             # unreserve everything and initialize res2
978             for move in picking.move_lines:
979                 quant_obj.quants_unreserve(cr, uid, move, context=context)
980                 res2[move.id] = move.product_qty
981                 resneg[move.id] = {}
982                 if move.state == 'assigned':
983                     products_moves.setdefault(move.product_id.id, []).append(move)
984                 
985                 
986             # Resort pack_operation_ids such that package transfers happen first and then the most specific operations from the product
987             
988             orderedpackops = picking.pack_operation_ids
989             orderedpackops.sort(key = lambda x: ((x.package_id and not x.product_id) and -3 or 0) + (x.package_id and -1 or 0) + (x.lot_id and -1 or 0))
990
991             for ops in orderedpackops:
992                 #If a product is specified in the ops, search for appropriate quants
993                 if ops.product_id:
994                     # Match with moves
995                     move_ids = ops.product_id.id in products_moves and filter(lambda x: res2[x.id] > 0, products_moves[ops.product_id.id]) or []
996                     qty_to_do = uom_obj._compute_qty(cr, uid, ops.product_uom_id.id, ops.product_qty, to_uom_id=ops.product_id.uom_id.id)
997                     while qty_to_do > 0 and move_ids:
998                         move = move_ids.pop()
999                         if res2[move.id] > qty_to_do: 
1000                             qty = qty_to_do
1001                             qty_to_do = 0
1002                         else:
1003                             qty = res2[move.id]
1004                             qty_to_do -= res2[move.id]
1005                         neg_qty = self._reserve_quants_ops_move(cr, uid, ops, move, qty, create=create, context=context)
1006                         if neg_qty > 0:
1007                             resneg[move.id].setdefault(ops.id, 0)
1008                             resneg [move.id][ops.id] += neg_qty
1009                         res2[move.id] -= qty
1010                     res[ops.id] = {}
1011                     res[ops.id][ops.product_id.id] = qty_to_do
1012                 # In case only a package is specified, take all the quants from the package
1013                 elif ops.package_id:
1014                     quants = quant_obj.browse(cr, uid, pack_obj.get_content(cr, uid, [ops.package_id.id], context=context))
1015                     quants = [x for x in quants if x.qty > 0] #Negative quants should not be moved
1016                     for quant in quants:
1017                         # Match with moves
1018                         move_ids = quant.product_id.id in products_moves and filter(lambda x: res2[x.id] > 0, products_moves[quant.product_id.id]) or []
1019                         qty_to_do = quant.qty
1020                         while qty_to_do > 0 and move_ids:
1021                             move = move_ids.pop()
1022                             if res2[move.id] > qty_to_do:
1023                                 qty = qty_to_do
1024                                 qty_to_do = 0.0
1025                             else:
1026                                 qty = res2[move.id]
1027                                 qty_to_do -= res2[move.id]
1028                             quant_obj.quants_reserve(cr, uid, [(quant, qty)], move, context=context)
1029                             quant_obj.write(cr, uid, [quant.id], {'reservation_op_id': ops.id}, context=context)
1030                             res2[move.id] -= qty
1031                         res.setdefault(ops.id, {}).setdefault(quant.product_id.id, 0.0)
1032                         res[ops.id][quant.product_id.id] += qty_to_do
1033         return (res, res2, resneg)
1034
1035     def do_partial(self, cr, uid, picking_ids, context=None):
1036         """
1037             If no pack operation, we do simple action_done of the picking
1038             Otherwise, do the pack operations
1039         """
1040         if not context:
1041             context={}
1042         stock_move_obj = self.pool.get('stock.move')
1043         for picking in self.browse(cr, uid, picking_ids, context=context):
1044             if not picking.pack_operation_ids:
1045                 self.action_done(cr, uid, [picking.id], context=context)
1046                 continue
1047             else:
1048                 # Rereserve quants
1049                 # TODO: quants could have been created already in Supplier, so create parameter could disappear
1050                 res = self.rereserve(cr, uid, [picking.id], create = True, context = context) #This time, quants need to be created 
1051                 resneg = res[2]
1052                 orig_moves = picking.move_lines
1053                 orig_qtys = {}
1054                 for orig in orig_moves:
1055                     orig_qtys[orig.id] = orig.product_qty
1056                 #Add moves that operations need extra
1057                 extra_moves = []
1058                 for ops in res[0].keys():
1059                     for prod in res[0][ops].keys():
1060                         product = self.pool.get('product.product').browse(cr, uid, prod, context=context)
1061                         qty = res[0][ops][prod]
1062                         if qty > 0:
1063                             #Create moves for products too many on operation
1064                             move_id = stock_move_obj.create(cr, uid, {
1065                                             'name': product.name,
1066                                             'product_id': product.id,
1067                                             'product_uom_qty': qty,
1068                                             'product_uom': product.uom_id.id,
1069                                             'location_id': picking.location_id.id,
1070                                             'location_dest_id': picking.location_dest_id.id,
1071                                             'picking_id': picking.id,
1072                                             'picking_type_id': picking.picking_type_id.id,
1073                                             'group_id': picking.group_id.id,
1074                                         }, context=context)
1075                             stock_move_obj.action_confirm(cr, uid, [move_id], context=context)
1076                             move = stock_move_obj.browse(cr, uid, move_id, context=context)
1077                             ops_rec = self.pool.get("stock.pack.operation").browse(cr, uid, ops, context=context)
1078                             resneg[move_id] = {}
1079                             resneg[move_id][ops] = self._reserve_quants_ops_move(cr, uid, ops_rec, move, qty, create=True, context=context)
1080                             extra_moves.append(move_id)
1081                 res2 = res[1]
1082                 #Backorder
1083                 for move in res2.keys():
1084                     if res2[move] > 0:
1085                         mov = stock_move_obj.browse(cr, uid, move, context=context)
1086                         new_move = stock_move_obj.split(cr, uid, mov, res2[move], context=context)
1087                         #Assign move as it was assigned before
1088                         stock_move_obj.action_assign(cr, uid, [new_move])
1089                 todo = []
1090                 orig_moves = [x for x in orig_moves if res[1][x.id] < orig_qtys[x.id]]
1091                 for move in orig_moves + stock_move_obj.browse(cr, uid, extra_moves, context=context):
1092                     if move.state == 'draft':
1093                         self.pool.get('stock.move').action_confirm(cr, uid, [move.id], context=context)
1094                         todo.append(move.id)
1095                     elif move.state in ('assigned','confirmed'):
1096                         todo.append(move.id)
1097                 if len(todo) and not ('do_only_split' in context and context['do_only_split']):
1098                     self.pool.get('stock.move').action_done(cr, uid, todo, negatives = resneg, context=context)
1099                 elif 'do_only_split' in context and context['do_only_split']:
1100                     context.update({'split': [x.id for x in orig_moves] + extra_moves})
1101             picking.refresh()
1102             self._create_backorder(cr, uid, picking, context=context)
1103         return True
1104
1105     def do_split(self, cr, uid, picking_ids, context=None):
1106         """
1107             just split the picking without making it 'done'
1108         """
1109         if context is None:
1110             context = {}
1111         ctx = context.copy()
1112         ctx['do_only_split'] = True
1113         self.do_partial(cr, uid, picking_ids, context=ctx)
1114         return True
1115
1116     # Methods for the barcode UI
1117
1118     def get_picking_for_packing_ui(self, cr, uid, context=None):
1119         return self.search(cr, uid, [('state', 'in', ('confirmed', 'assigned')), ('picking_type_id', '=', context.get('default_picking_type_id'))], context=context)
1120
1121     def action_done_from_packing_ui(self, cr, uid, picking_id, only_split_lines=False, context=None):
1122         self.do_partial(cr, uid, picking_id, only_split_lines, context=context)
1123         #return id of next picking to work on
1124         return self.get_picking_for_packing_ui(cr, uid, context=context)
1125
1126     def action_pack(self, cr, uid, picking_ids, context=None):
1127         stock_operation_obj = self.pool.get('stock.pack.operation')
1128         package_obj = self.pool.get('stock.quant.package')
1129         for picking_id in picking_ids:
1130             operation_ids = stock_operation_obj.search(cr, uid, [('picking_id', '=', picking_id), ('result_package_id', '=', False)], context=context)
1131             if operation_ids:
1132                 package_id = package_obj.create(cr, uid, {}, context=context)
1133                 stock_operation_obj.write(cr, uid, operation_ids, {'result_package_id': package_id}, context=context)
1134         return True
1135
1136     def _deal_with_quants(self, cr, uid, picking_id, quant_ids, context=None):
1137         stock_operation_obj = self.pool.get('stock.pack.operation')
1138         todo_on_moves = []
1139         todo_on_operations = []
1140         for quant in self.pool.get('stock.quant').browse(cr, uid, quant_ids, context=context):
1141             tmp_moves, tmp_operations = stock_operation_obj._search_and_increment(cr, uid, picking_id, ('quant_id', '=', quant.id), context=context)
1142             todo_on_moves += tmp_moves
1143             todo_on_operations += tmp_operations
1144         return todo_on_moves, todo_on_operations
1145
1146     def get_barcode_and_return_todo_stuff(self, cr, uid, picking_id, barcode_str, context=None):
1147         '''This function is called each time there barcode scanner reads an input'''
1148         #TODO: better error messages handling => why not real raised errors
1149         quant_obj = self.pool.get('stock.quant')
1150         package_obj = self.pool.get('stock.quant.package')
1151         product_obj = self.pool.get('product.product')
1152         stock_operation_obj = self.pool.get('stock.pack.operation')
1153         error_msg = ''
1154         todo_on_moves = []
1155         todo_on_operations = []
1156         #check if the barcode correspond to a product
1157         matching_product_ids = product_obj.search(cr, uid, [('ean13', '=', barcode_str)], context=context)
1158         if matching_product_ids:
1159             todo_on_moves, todo_on_operations = stock_operation_obj._search_and_increment(cr, uid, picking_id, ('product_id', '=', matching_product_ids[0]), context=context)
1160
1161         #check if the barcode correspond to a quant
1162         matching_quant_ids = quant_obj.search(cr, uid, [('name', '=', barcode_str)], context=context)  # TODO need the location clause
1163         if matching_quant_ids:
1164             todo_on_moves, todo_on_operations = self._deal_with_quants(cr, uid, picking_id, [matching_quant_ids[0]], context=context)
1165
1166         #check if the barcode correspond to a package
1167         matching_package_ids = package_obj.search(cr, uid, [('name', '=', barcode_str)], context=context)
1168         if matching_package_ids:
1169             included_package_ids = package_obj.search(cr, uid, [('parent_id', 'child_of', matching_package_ids[0])], context=context)
1170             included_quant_ids = quant_obj.search(cr, uid, [('package_id', 'in', included_package_ids)], context=context)
1171             todo_on_moves, todo_on_operations = self._deal_with_quants(cr, uid, picking_id, included_quant_ids, context=context)
1172         #write remaining qty on stock.move, to ease the treatment server side
1173         for todo in todo_on_moves:
1174             if todo[0] == 1:
1175                 self.pool.get('stock.move').write(cr, uid, todo[1], todo[2], context=context)
1176             elif todo[0] == 0:
1177                 self.pool.get('stock.move').create(cr, uid, todo[2], context=context)
1178         return {'warnings': error_msg, 'moves_to_update': todo_on_moves, 'operations_to_update': todo_on_operations}
1179
1180
1181 class stock_production_lot(osv.osv):
1182     _name = 'stock.production.lot'
1183     _inherit = ['mail.thread']
1184     _description = 'Lot/Serial'
1185     _columns = {
1186         'name': fields.char('Serial Number', size=64, required=True, help="Unique Serial Number"),
1187         'ref': fields.char('Internal Reference', size=256, help="Internal reference number in case it differs from the manufacturer's serial number"),
1188         'product_id': fields.many2one('product.product', 'Product', required=True, domain=[('type', '<>', 'service')]),
1189         'quant_ids': fields.one2many('stock.quant', 'lot_id', 'Quants'),
1190         'create_date': fields.datetime('Creation Date'),
1191     }
1192     _defaults = {
1193         'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'stock.lot.serial'),
1194         'product_id': lambda x, y, z, c: c.get('product_id', False),
1195     }
1196     _sql_constraints = [
1197         ('name_ref_uniq', 'unique (name, ref)', 'The combination of Serial Number and internal reference must be unique !'),
1198     ]
1199
1200
1201 # ----------------------------------------------------
1202 # Move
1203 # ----------------------------------------------------
1204
1205 class stock_move(osv.osv):
1206     _name = "stock.move"
1207     _description = "Stock Move"
1208     _order = 'date_expected desc, id'
1209     _log_create = False
1210
1211     def get_price_unit(self, cr, uid, move, context=None):
1212         """ Returns the unit price to store on the quant """
1213         return move.price_unit or move.product_id.standard_price
1214
1215     def name_get(self, cr, uid, ids, context=None):
1216         res = []
1217         for line in self.browse(cr, uid, ids, context=context):
1218             name = line.location_id.name + ' > ' + line.location_dest_id.name
1219             if line.product_id.code:
1220                 name = line.product_id.code + ': ' + name
1221             if line.picking_id.origin:
1222                 name = line.picking_id.origin + '/ ' + name
1223             res.append((line.id, name))
1224         return res
1225
1226     def _quantity_normalize(self, cr, uid, ids, name, args, context=None):
1227         uom_obj = self.pool.get('product.uom')
1228         res = {}
1229         for m in self.browse(cr, uid, ids, context=context):
1230             res[m.id] = uom_obj._compute_qty_obj(cr, uid, m.product_uom, m.product_uom_qty, m.product_id.uom_id, round=False)
1231         return res
1232
1233     def _get_remaining_qty(self, cr, uid, ids, field_name, args, context=None):
1234         res = {}
1235         for move in self.browse(cr, uid, ids, context=context):
1236             res[move.id] = move.product_qty
1237             for quant in move.reserved_quant_ids:
1238                 res[move.id] -= quant.qty
1239         return res
1240
1241     def _get_lot_ids(self, cr, uid, ids, field_name, args, context=None):
1242         res = dict.fromkeys(ids, False)
1243         for move in self.browse(cr, uid, ids, context=context):
1244             if move.state == 'done':
1245                 res[move.id] = [q.id for q in move.quant_ids]
1246             else:
1247                 res[move.id] = [q.id for q in move.reserved_quant_ids]
1248         return res
1249
1250     def _get_product_availability(self, cr, uid, ids, field_name, args, context=None):
1251         quant_obj = self.pool.get('stock.quant')
1252         res = dict.fromkeys(ids, False)
1253         for move in self.browse(cr, uid, ids, context=context):
1254             if move.state == 'done':
1255                 res[move.id] = move.product_qty
1256             else:
1257                 sublocation_ids = self.pool.get('stock.location').search(cr, uid, [('id', 'child_of', [move.location_id.id])], context=context)
1258                 quant_ids = quant_obj.search(cr, uid, [('location_id', 'in', sublocation_ids), ('product_id', '=', move.product_id.id), ('reservation_id', '=', False)], context=context)
1259                 availability = 0
1260                 for quant in quant_obj.browse(cr, uid, quant_ids, context=context):
1261                     availability += quant.qty
1262                 res[move.id] = min(move.product_qty, availability)
1263         return res
1264
1265     def _get_move(self, cr, uid, ids, context=None):
1266         res = set()
1267         for quant in self.browse(cr, uid, ids, context=context):
1268             if quant.reservation_id:
1269                 res.add(quant.reservation_id.id)
1270         return list(res)
1271
1272     _columns = {
1273         'name': fields.char('Description', required=True, select=True),
1274         'priority': fields.selection([('0', 'Not urgent'), ('1', 'Urgent')], 'Priority'),
1275         'create_date': fields.datetime('Creation Date', readonly=True, select=True),
1276         '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)]}),
1277         'date_expected': fields.datetime('Scheduled Date', states={'done': [('readonly', True)]},required=True, select=True, help="Scheduled date for the processing of this move"),
1278         'product_id': fields.many2one('product.product', 'Product', required=True, select=True, domain=[('type','<>','service')],states={'done': [('readonly', True)]}),
1279         # TODO: improve store to add dependency on product UoM
1280         'product_qty': fields.function(_quantity_normalize, type='float', store=True, string='Quantity',
1281             digits_compute=dp.get_precision('Product Unit of Measure'),
1282             help='Quantity in the default UoM of the product'),
1283         'product_uom_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'),
1284             required=True,states={'done': [('readonly', True)]},
1285             help="This is the quantity of products from an inventory "
1286                 "point of view. For moves in the state 'done', this is the "
1287                 "quantity of products that were actually moved. For other "
1288                 "moves, this is the quantity of product that is planned to "
1289                 "be moved. Lowering this quantity does not generate a "
1290                 "backorder. Changing this quantity on assigned moves affects "
1291                 "the product reservation, and should be done with care."
1292         ),
1293         'product_uom': fields.many2one('product.uom', 'Unit of Measure', required=True,states={'done': [('readonly', True)]}),
1294         'product_uos_qty': fields.float('Quantity (UOS)', digits_compute=dp.get_precision('Product Unit of Measure'), states={'done': [('readonly', True)]}),
1295         'product_uos': fields.many2one('product.uom', 'Product UOS', states={'done': [('readonly', True)]}),
1296
1297         'product_packaging': fields.many2one('product.packaging', 'Prefered Packaging', help="It specifies attributes of packaging like type, quantity of packaging,etc."),
1298
1299         '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."),
1300         '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."),
1301
1302         # FP Note: should we remove this?
1303         '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"),
1304
1305
1306         'move_dest_id': fields.many2one('stock.move', 'Destination Move', help="Optional: next stock move when chaining them", select=True),
1307         'move_orig_ids': fields.one2many('stock.move', 'move_dest_id', 'Original Move', help="Optional: previous stock move when chaining them", select=True),
1308
1309         'picking_id': fields.many2one('stock.picking', 'Reference', select=True, states={'done': [('readonly', True)]}),
1310         'picking_priority': fields.related('picking_id','priority', type='selection', selection=[('0','Low'),('1','Normal'),('2','High')], string='Picking Priority'),
1311         'note': fields.text('Notes'),
1312         'state': fields.selection([('draft', 'New'),
1313                                    ('cancel', 'Cancelled'),
1314                                    ('waiting', 'Waiting Another Move'),
1315                                    ('confirmed', 'Waiting Availability'),
1316                                    ('assigned', 'Available'),
1317                                    ('done', 'Done'),
1318                                    ], 'Status', readonly=True, select=True,
1319                  help= "* New: When the stock move is created and not yet confirmed.\n"\
1320                        "* Waiting Another Move: This state can be seen when a move is waiting for another one, for example in a chained flow.\n"\
1321                        "* 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"\
1322                        "* Available: When products are reserved, it is set to \'Available\'.\n"\
1323                        "* Done: When the shipment is processed, the state is \'Done\'."),
1324
1325         'price_unit': fields.float('Unit Price', help="Technical field used to record the product cost set by the user during a picking confirmation (when average price costing method is used). Value given in company currency and in product uom."),  # as it's a technical field, we intentionally don't provide the digits attribute
1326
1327         'company_id': fields.many2one('res.company', 'Company', required=True, select=True),
1328         'backorder_id': fields.related('picking_id','backorder_id',type='many2one', relation="stock.picking", string="Back Order of", select=True),
1329         'origin': fields.char("Source"),
1330         'procure_method': fields.selection([('make_to_stock','Make to Stock'),('make_to_order','Make to Order')], 'Procurement Method', required=True, help="Make to Stock: When needed, the product is taken from the stock or we wait for replenishment. \nMake to Order: When needed, the product is purchased or produced."),
1331
1332         # used for colors in tree views:
1333         'scrapped': fields.related('location_dest_id','scrap_location',type='boolean',relation='stock.location',string='Scrapped', readonly=True),
1334
1335         'quant_ids': fields.many2many('stock.quant',  'stock_quant_move_rel', 'move_id', 'quant_id', 'Quants'),
1336         'reserved_quant_ids': fields.one2many('stock.quant', 'reservation_id', 'Reserved quants'),
1337         'remaining_qty': fields.function(_get_remaining_qty, type='float', string='Remaining Quantity', 
1338                                          digits_compute=dp.get_precision('Product Unit of Measure'), states={'done': [('readonly', True)]},
1339                                          store = {'stock.move': (lambda self, cr, uid, ids, c={}: ids , ['product_uom_qty', 'product_uom', 'reserved_quant_ids'], 20), 
1340                                                   'stock.quant': (_get_move, ['reservation_id'], 10)}),
1341         'procurement_id': fields.many2one('procurement.order', 'Procurement'),
1342         'group_id': fields.many2one('procurement.group', 'Procurement Group'),
1343         'rule_id': fields.many2one('procurement.rule', 'Procurement Rule', help='The pull rule that created this stock move'),
1344         'propagate': fields.boolean('Propagate cancel and split', help='If checked, when this move is cancelled, cancel the linked move too'),
1345         'picking_type_id': fields.many2one('stock.picking.type', 'Picking Type'),
1346         'inventory_id': fields.many2one('stock.inventory', 'Inventory'),
1347         'lot_ids': fields.function(_get_lot_ids, type='many2many', relation='stock.quant', string='Lots'),
1348         'origin_returned_move_id': fields.many2one('stock.move', 'Origin return move', help='move that created the return move'),
1349         'returned_move_ids': fields.one2many('stock.move', 'origin_returned_move_id', 'All returned moves', help='Optional: all returned moves created from this move'),
1350         'availability': fields.function(_get_product_availability, type='float', string='Availability'),
1351         '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'"),
1352         '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'"),
1353         'putaway_ids': fields.one2many('stock.move.putaway', 'move_id', 'Put Away Suggestions'), 
1354         '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"),
1355         '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)."),
1356     }
1357
1358     def _default_location_destination(self, cr, uid, context=None):
1359         context = context or {}
1360         if context.get('default_picking_type_id', False):
1361             pick_type = self.pool.get('stock.picking.type').browse(cr, uid, context['default_picking_type_id'], context=context)
1362             return pick_type.default_location_dest_id and pick_type.default_location_dest_id.id or False
1363         return False
1364
1365     def _default_location_source(self, cr, uid, context=None):
1366         context = context or {}
1367         if context.get('default_picking_type_id', False):
1368             pick_type = self.pool.get('stock.picking.type').browse(cr, uid, context['default_picking_type_id'], context=context)
1369             return pick_type.default_location_src_id and pick_type.default_location_src_id.id or False
1370         return False
1371
1372     def _default_destination_address(self, cr, uid, context=None):
1373         user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
1374         return user.company_id.partner_id.id
1375
1376     _defaults = {
1377         'location_id': _default_location_source,
1378         'location_dest_id': _default_location_destination,
1379         'partner_id': _default_destination_address,
1380         'state': 'draft',
1381         'priority': '1',
1382         'product_qty': 1.0,
1383         'product_uom_qty': 1.0,
1384         'scrapped': False,
1385         'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
1386         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.move', context=c),
1387         'date_expected': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
1388         'procure_method': 'make_to_stock',
1389         'propagate': True,
1390     }
1391
1392     def _check_uom(self, cr, uid, ids, context=None):
1393         for move in self.browse(cr, uid, ids, context=context):
1394             if move.product_id.uom_id.category_id.id != move.product_uom.category_id.id:
1395                 return False
1396         return True
1397
1398     _constraints = [
1399         (_check_uom,
1400             '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.',
1401             ['product_uom'])]
1402
1403     def copy(self, cr, uid, id, default=None, context=None):
1404         if default is None:
1405             default = {}
1406         default = default.copy()
1407         default['move_orig_ids'] = []
1408         default['quant_ids'] = []
1409         default['reserved_quant_ids'] = []
1410         default['returned_move_ids'] = []
1411         default['origin_returned_move_id'] = False
1412         default['state'] = 'draft'
1413         return super(stock_move, self).copy(cr, uid, id, default, context)
1414
1415     def _prepare_procurement_from_move(self, cr, uid, move, context=None):
1416         origin = (move.group_id and (move.group_id.name + ":") or "") + (move.rule_id and move.rule_id.name or "/")
1417         group_id = move.group_id and move.group_id.id or False
1418         if move.rule_id:
1419             if move.rule_id.group_propagation_option == 'fixed' and move.rule_id.group_id:
1420                 group_id = move.rule_id.group_id.id
1421             elif move.rule_id.group_propagation_option == 'none':
1422                 group_id = False
1423         return {
1424             'name': move.rule_id and move.rule_id.name or "/",
1425             'origin': origin,
1426             'company_id': move.company_id and move.company_id.id or False,
1427             'date_planned': move.date,
1428             'product_id': move.product_id.id,
1429             'product_qty': move.product_qty,
1430             'product_uom': move.product_uom.id,
1431             'product_uos_qty': (move.product_uos and move.product_uos_qty) or move.product_qty,
1432             'product_uos': (move.product_uos and move.product_uos.id) or move.product_uom.id,
1433             'location_id': move.location_id.id,
1434             'move_dest_id': move.id,
1435             'group_id': group_id,
1436             'route_ids': [(4, x.id) for x in move.route_ids],
1437             'warehouse_id': move.warehouse_id and move.warehouse_id.id or False,
1438         }
1439
1440     def _push_apply(self, cr, uid, moves, context):
1441         push_obj = self.pool.get("stock.location.path")
1442         for move in moves:
1443             if not move.move_dest_id:
1444                 routes = [x.id for x in move.product_id.route_ids + move.product_id.categ_id.total_route_ids]
1445                 routes = routes or [x.id for x in move.route_ids]  
1446                 if routes:
1447                     domain = [('route_id', 'in', routes), ('location_from_id', '=', move.location_dest_id.id)]
1448                     if move.warehouse_id:
1449                         domain += [('warehouse_id', '=', move.warehouse_id.id)]
1450                     rules = push_obj.search(cr, uid, domain, context=context)
1451                     if rules:
1452                         rule = push_obj.browse(cr, uid, rules[0], context=context)
1453                         push_obj._apply(cr, uid, rule, move, context=context)
1454         return True
1455
1456     # Create the stock.move.putaway records
1457     def _putaway_apply(self,cr, uid, ids, context=None):
1458         moveputaway_obj = self.pool.get('stock.move.putaway')
1459         for move in self.browse(cr, uid, ids, context=context):
1460             putaway = self.pool.get('stock.location').get_putaway_strategy(cr, uid, move.location_dest_id, move.product_id, context=context)
1461             if putaway:
1462                 # Should call different methods here in later versions
1463                 # TODO: take care of lots
1464                 if putaway.method == 'fixed' and putaway.location_spec_id:
1465                     moveputaway_obj.create(cr, SUPERUSER_ID, {'move_id': move.id,
1466                                                      'location_id': putaway.location_spec_id.id,
1467                                                      'quantity': move.product_uom_qty}, context=context)
1468         return True
1469     
1470     def _create_procurement(self, cr, uid, move, context=None):
1471         """
1472             This will create a procurement order
1473         """
1474         proc_obj = self.pool.get("procurement.order")
1475         return proc_obj.create(cr, uid, self._prepare_procurement_from_move(cr, uid, move, context=context))
1476
1477     # Check that we do not modify a stock.move which is done
1478     def write(self, cr, uid, ids, vals, context=None):
1479         if isinstance(ids, (int, long)):
1480             ids = [ids]
1481         frozen_fields = set(['product_qty', 'product_uom', 'product_uos_qty', 'product_uos', 'location_id', 'location_dest_id', 'product_id'])
1482         for move in self.browse(cr, uid, ids, context=context):
1483             if move.state == 'done':
1484                 if frozen_fields.intersection(vals):
1485                     raise osv.except_osv(_('Operation Forbidden!'),
1486                         _('Quantities, Units of Measure, Products and Locations cannot be modified on stock moves that have already been processed (except by the Administrator).'))
1487         result = super(stock_move, self).write(cr, uid, ids, vals, context=context)
1488         return result
1489
1490     def onchange_quantity(self, cr, uid, ids, product_id, product_qty,
1491                           product_uom, product_uos):
1492         """ On change of product quantity finds UoM and UoS quantities
1493         @param product_id: Product id
1494         @param product_qty: Changed Quantity of product
1495         @param product_uom: Unit of measure of product
1496         @param product_uos: Unit of sale of product
1497         @return: Dictionary of values
1498         """
1499         result = {
1500             'product_uos_qty': 0.00
1501         }
1502         warning = {}
1503
1504         if (not product_id) or (product_qty <=0.0):
1505             result['product_qty'] = 0.0
1506             return {'value': result}
1507
1508         product_obj = self.pool.get('product.product')
1509         uos_coeff = product_obj.read(cr, uid, product_id, ['uos_coeff'])
1510
1511         # Warn if the quantity was decreased
1512         if ids:
1513             for move in self.read(cr, uid, ids, ['product_qty']):
1514                 if product_qty < move['product_qty']:
1515                     warning.update({
1516                        'title': _('Information'),
1517                        'message': _("By changing this quantity here, you accept the "
1518                                 "new quantity as complete: OpenERP will not "
1519                                 "automatically generate a back order.") })
1520                 break
1521
1522         if product_uos and product_uom and (product_uom != product_uos):
1523             result['product_uos_qty'] = product_qty * uos_coeff['uos_coeff']
1524         else:
1525             result['product_uos_qty'] = product_qty
1526
1527         return {'value': result, 'warning': warning}
1528
1529     def onchange_uos_quantity(self, cr, uid, ids, product_id, product_uos_qty,
1530                           product_uos, product_uom):
1531         """ On change of product quantity finds UoM and UoS quantities
1532         @param product_id: Product id
1533         @param product_uos_qty: Changed UoS Quantity of product
1534         @param product_uom: Unit of measure of product
1535         @param product_uos: Unit of sale of product
1536         @return: Dictionary of values
1537         """
1538         result = {
1539             'product_uom_qty': 0.00
1540         }
1541         warning = {}
1542
1543         if (not product_id) or (product_uos_qty <=0.0):
1544             result['product_uos_qty'] = 0.0
1545             return {'value': result}
1546
1547         product_obj = self.pool.get('product.product')
1548         uos_coeff = product_obj.read(cr, uid, product_id, ['uos_coeff'])
1549
1550         # Warn if the quantity was decreased
1551         for move in self.read(cr, uid, ids, ['product_uos_qty']):
1552             if product_uos_qty < move['product_uos_qty']:
1553                 warning.update({
1554                    'title': _('Warning: No Back Order'),
1555                    'message': _("By changing the quantity here, you accept the "
1556                                 "new quantity as complete: OpenERP will not "
1557                                 "automatically generate a Back Order.") })
1558                 break
1559
1560         if product_uos and product_uom and (product_uom != product_uos):
1561             result['product_uom_qty'] = product_uos_qty / uos_coeff['uos_coeff']
1562         else:
1563             result['product_uom_qty'] = product_uos_qty
1564         return {'value': result, 'warning': warning}
1565
1566     def onchange_product_id(self, cr, uid, ids, prod_id=False, loc_id=False,
1567                             loc_dest_id=False, partner_id=False):
1568         """ On change of product id, if finds UoM, UoS, quantity and UoS quantity.
1569         @param prod_id: Changed Product id
1570         @param loc_id: Source location id
1571         @param loc_dest_id: Destination location id
1572         @param partner_id: Address id of partner
1573         @return: Dictionary of values
1574         """
1575         if not prod_id:
1576             return {}
1577         user = self.pool.get('res.users').browse(cr, uid, uid)
1578         lang = user and user.lang or False
1579         if partner_id:
1580             addr_rec = self.pool.get('res.partner').browse(cr, uid, partner_id)
1581             if addr_rec:
1582                 lang = addr_rec and addr_rec.lang or False
1583         ctx = {'lang': lang}
1584
1585         product = self.pool.get('product.product').browse(cr, uid, [prod_id], context=ctx)[0]
1586         uos_id  = product.uos_id and product.uos_id.id or False
1587         result = {
1588             'product_uom': product.uom_id.id,
1589             'product_uos': uos_id,
1590             'product_uom_qty': 1.00,
1591             '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'],
1592         }
1593         if not ids:
1594             result['name'] = product.partner_ref
1595         if loc_id:
1596             result['location_id'] = loc_id
1597         if loc_dest_id:
1598             result['location_dest_id'] = loc_dest_id
1599         return {'value': result}
1600
1601     def _picking_assign(self, cr, uid, move, context=None):
1602         if move.picking_id or not move.picking_type_id:
1603             return False
1604         context = context or {}
1605         pick_obj = self.pool.get("stock.picking")
1606         picks = []
1607         group = move.group_id and move.group_id.id or False
1608         picks = pick_obj.search(cr, uid, [
1609                 ('group_id', '=', group),
1610                 ('location_id', '=', move.location_id.id),
1611                 ('location_dest_id', '=', move.location_dest_id.id),
1612                 ('state', 'in', ['draft', 'confirmed', 'waiting'])], context=context)
1613         if picks:
1614             pick = picks[0]
1615         else:
1616             values = {
1617                 'origin': move.origin,
1618                 'company_id': move.company_id and move.company_id.id or False,
1619                 'move_type': move.group_id and move.group_id.move_type or 'one',
1620                 'partner_id': move.group_id and move.group_id.partner_id and move.group_id.partner_id.id or False,
1621                 'date_done': move.date_expected,
1622                 'picking_type_id': move.picking_type_id and move.picking_type_id.id or False,
1623             }
1624             pick = pick_obj.create(cr, uid, values, context=context)
1625         move.write({'picking_id': pick})
1626         return True
1627
1628     def onchange_date(self, cr, uid, ids, date, date_expected, context=None):
1629         """ On change of Scheduled Date gives a Move date.
1630         @param date_expected: Scheduled Date
1631         @param date: Move Date
1632         @return: Move Date
1633         """
1634         if not date_expected:
1635             date_expected = time.strftime('%Y-%m-%d %H:%M:%S')
1636         return {'value':{'date': date_expected}}
1637
1638     def action_confirm(self, cr, uid, ids, context=None):
1639         """ Confirms stock move or put it in waiting if it's linked to another move.
1640         @return: List of ids.
1641         """
1642         states = {
1643             'confirmed': [],
1644             'waiting': []
1645         }
1646         for move in self.browse(cr, uid, ids, context=context):
1647             state = 'confirmed'
1648             for m in move.move_orig_ids:
1649                 if m.state not in ('done', 'cancel'):
1650                     state = 'waiting'
1651             states[state].append(move.id)
1652             self._picking_assign(cr, uid, move, context=context)
1653
1654         for state, write_ids in states.items():
1655             if len(write_ids):
1656                 self.write(cr, uid, write_ids, {'state': state})
1657                 if state == 'confirmed':
1658                     for move in self.browse(cr, uid, write_ids, context=context):
1659                         if move.procure_method == 'make_to_order':
1660                             self._create_procurement(cr, uid, move, context=context)
1661         moves = self.browse(cr, uid, ids, context=context)
1662         self._push_apply(cr, uid, moves, context=context)
1663         return True
1664
1665     def force_assign(self, cr, uid, ids, context=None):
1666         """ Changes the state to assigned.
1667         @return: True
1668         """
1669         done = self.action_assign(cr, uid, ids, context=context)
1670         self.write(cr, uid, list(set(ids) - set(done)), {'state': 'assigned'})
1671         return True
1672
1673
1674     def cancel_assign(self, cr, uid, ids, context=None):
1675         """ Changes the state to confirmed.
1676         @return: True
1677         """
1678         return self.write(cr, uid, ids, {'state': 'confirmed'})
1679
1680     def action_assign(self, cr, uid, ids, context=None):
1681         """ Checks the product type and accordingly writes the state.
1682         @return: No. of moves done
1683         """
1684         context = context or {}
1685         quant_obj = self.pool.get("stock.quant")
1686         done = []
1687         for move in self.browse(cr, uid, ids, context=context):
1688             if move.state not in ('confirmed', 'waiting'):
1689                 continue
1690             if move.product_id.type == 'consu':
1691                 done.append(move.id)
1692                 continue
1693             else:
1694                 qty = move.product_qty
1695                 dp = []
1696                 for m2 in move.move_orig_ids:
1697                     for q in m2.quant_ids:
1698                         dp.append(str(q.id))
1699                         qty -= q.qty
1700                 domain = ['|', ('reservation_id', '=', False), ('reservation_id', '=', move.id), ('qty', '>', 0)]
1701                 prefered_domain = dp and [('id', 'not in', dp)] or []
1702                 fallback_domain = dp and [('id', 'in', dp)] or []
1703                 quants = quant_obj.quants_get_prefered_domain(cr, uid, move.location_id, move.product_id, qty, domain=domain, prefered_domain=prefered_domain, fallback_domain=fallback_domain, restrict_lot_id=move.restrict_lot_id.id, restrict_partner_id=move.restrict_partner_id.id, context=context)
1704                 #Will only reserve physical quants, no negative
1705                 quant_obj.quants_reserve(cr, uid, quants, move, context=context)
1706                 # the total quantity is provided by existing quants
1707                 if all(map(lambda x:x[0], quants)):
1708                     done.append(move.id)
1709         self.write(cr, uid, done, {'state': 'assigned'})
1710         self._putaway_apply(cr, uid, ids, context=context)        
1711         return done
1712
1713
1714     #
1715     # Cancel move => cancel others move and pickings
1716     #
1717     def action_cancel(self, cr, uid, ids, context=None):
1718         """ Cancels the moves and if all moves are cancelled it cancels the picking.
1719         @return: True
1720         """
1721         procurement_obj = self.pool.get('procurement.order')
1722         context = context or {}
1723         for move in self.browse(cr, uid, ids, context=context):
1724             if move.state == 'done':
1725                 raise osv.except_osv(_('Operation Forbidden!'),
1726                         _('You cannot cancel a stock move that has been set to \'Done\'.'))
1727             if move.reserved_quant_ids:
1728                 self.pool.get("stock.quant").quants_unreserve(cr, uid, move, context=context)
1729             if context.get('cancel_procurement'):
1730                 if move.propagate:
1731                     procurement_ids = procurement_obj.search(cr, uid, [('move_dest_id', '=', move.id)], context=context)
1732                     procurement_obj.cancel(cr, uid, procurement_ids, context=context)
1733             elif move.move_dest_id:
1734                 #cancel chained moves
1735                 if move.propagate:
1736                     self.action_cancel(cr, uid, [move.move_dest_id.id], context=context)
1737                 elif move.move_dest_id.state == 'waiting':
1738                     self.write(cr, uid, [move.move_dest_id.id], {'state': 'confirmed'})
1739         return self.write(cr, uid, ids, {'state': 'cancel', 'move_dest_id': False})
1740
1741     #def _get_quants_from_pack(self, cr, uid, ids, context=None):
1742     #    """
1743     #    Suppose for the moment we don't have any packaging
1744     #    """
1745     #    res = {}
1746     #    for move in self.browse(cr, uid, ids, context=context):
1747     #        #Split according to pack wizard if necessary
1748     #        res[move.id] = [x.id for x in move.reserved_quant_ids]
1749     #    return res
1750
1751     def action_done(self, cr, uid, ids, negatives = False, context=None):
1752         """ Makes the move done and if all moves are done, it will finish the picking.
1753         If quants are not assigned yet, it should assign them
1754         Putaway strategies should be applied
1755         @return:
1756         """
1757         context = context or {}
1758         quant_obj = self.pool.get("stock.quant")
1759         ops_obj = self.pool.get("stock.pack.operation")
1760         pack_obj = self.pool.get("stock.quant.package")
1761         todo = [move.id for move in self.browse(cr, uid, ids, context=context) if move.state == "draft"]
1762         if todo:
1763             self.action_confirm(cr, uid, todo, context=context)
1764
1765         pickings = set()
1766         procurement_ids = []
1767         for move in self.browse(cr, uid, ids, context=context):
1768 #             if negatives and negatives[move.id]:
1769 #                 for ops in negatives[move.id].keys():
1770 #                 quants_to_move = [(None, negatives[move.id, x) for x in negatives]
1771 #                 quant_obj.quants_move(cr, uid, quants_to_move, move, context=context)
1772
1773             if move.picking_id:
1774                 pickings.add(move.picking_id.id)
1775             qty = move.product_qty
1776
1777             # for qty, location_id in move_id.prefered_location_ids:
1778             #    quants = quant_obj.quants_get(cr, uid, move.location_id, move.product_id, qty, context=context)
1779             #    quant_obj.quants_move(cr, uid, quants, move, location_dest_id, context=context)
1780             # should replace the above 2 lines
1781             dom = [('qty', '>', 0)]
1782             prefered_domain = [('reservation_id', '=', move.id)]
1783             fallback_domain = [('reservation_id', '=', False)]
1784             if move.picking_id and move.picking_id.pack_operation_ids:
1785                 quants = quant_obj.quants_get_prefered_domain(cr, uid, move.location_id, move.product_id, qty - move.remaining_qty, domain=dom, prefered_domain=prefered_domain, fallback_domain=fallback_domain, context=context)
1786                 quant_obj.quants_move(cr, uid, quants, move, context=context)
1787                 if negatives and move.id in negatives:
1788                     for negative_op in negatives[move.id].keys():
1789                         ops = ops_obj.browse(cr, uid, negative_op, context=context)
1790                         negatives[move.id][negative_op] = quant_obj.quants_move(cr, uid, [(None, negatives[move.id][negative_op])], move, 
1791                                                                                 lot_id = ops.lot_id and ops.lot_id.id or False, 
1792                                                                                 owner_id = ops.owner_id and ops.owner_id.id or False, 
1793                                                                                 package_id = ops.package_id and ops.package_id.id or False, context=context)
1794                 #Packing:
1795                 reserved_ops = list(set([x.reservation_op_id.id for x in move.reserved_quant_ids]))
1796                 for ops in ops_obj.browse(cr, uid, reserved_ops, context=context):
1797                     if ops.product_id:
1798                         quant_obj.write(cr, uid, [x.id for x in ops.reserved_quant_ids], {'package_id': ops.result_package_id and ops.result_package_id.id or False}, context=context)
1799                     else:
1800                         pack_obj.write(cr, uid, [ops.package_id.id], {'parent_id': ops.result_package_id and ops.result_package_id.id or False}, context=context)
1801             else:
1802                 quants = quant_obj.quants_get_prefered_domain(cr, uid, move.location_id, move.product_id, qty, domain=dom, prefered_domain=prefered_domain, fallback_domain=fallback_domain, context=context)
1803                 #Will move all quants_get and as such create negative quants
1804                 quant_obj.quants_move(cr, uid, quants, move, context=context)
1805             quant_obj.quants_unreserve(cr, uid, move, context=context)
1806
1807             #Check moves that were pushed
1808             if move.move_dest_id.state in ('waiting', 'confirmed'):
1809                 other_upstream_move_ids = self.search(cr, uid, [('id', '!=', move.id), ('state', 'not in', ['done', 'cancel']),
1810                                             ('move_dest_id', '=', move.move_dest_id.id)], context=context)
1811                 #If no other moves for the move that got pushed:
1812                 if not other_upstream_move_ids and move.move_dest_id.state in ('waiting', 'confirmed'):
1813                     self.action_assign(cr, uid, [move.move_dest_id.id], context=context)
1814             if move.procurement_id:
1815                 procurement_ids.append(move.procurement_id.id)
1816         self.write(cr, uid, ids, {'state': 'done', 'date': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context)
1817         self.pool.get('procurement.order').check(cr, uid, procurement_ids, context=context)
1818         return True
1819
1820     def unlink(self, cr, uid, ids, context=None):
1821         context = context or {}
1822         for move in self.browse(cr, uid, ids, context=context):
1823             if move.state not in ('draft', 'cancel'):
1824                 raise osv.except_osv(_('User Error!'), _('You can only delete draft moves.'))
1825         return super(stock_move, self).unlink(cr, uid, ids, context=context)
1826
1827     def action_scrap(self, cr, uid, ids, quantity, location_id, context=None):
1828         """ Move the scrap/damaged product into scrap location
1829         @param cr: the database cursor
1830         @param uid: the user id
1831         @param ids: ids of stock move object to be scrapped
1832         @param quantity : specify scrap qty
1833         @param location_id : specify scrap location
1834         @param context: context arguments
1835         @return: Scraped lines
1836         """
1837         #quantity should in MOVE UOM
1838         if quantity <= 0:
1839             raise osv.except_osv(_('Warning!'), _('Please provide a positive quantity to scrap.'))
1840         res = []
1841         for move in self.browse(cr, uid, ids, context=context):
1842             source_location = move.location_id
1843             if move.state == 'done':
1844                 source_location = move.location_dest_id
1845             #Previously used to prevent scraping from virtual location but not necessary anymore
1846             #if source_location.usage != 'internal':
1847                 #restrict to scrap from a virtual location because it's meaningless and it may introduce errors in stock ('creating' new products from nowhere)
1848                 #raise osv.except_osv(_('Error!'), _('Forbidden operation: it is not allowed to scrap products from a virtual location.'))
1849             move_qty = move.product_qty
1850             uos_qty = quantity / move_qty * move.product_uos_qty
1851             default_val = {
1852                 'location_id': source_location.id,
1853                 'product_uom_qty': quantity,
1854                 'product_uos_qty': uos_qty,
1855                 'state': move.state,
1856                 'scrapped': True,
1857                 'location_dest_id': location_id,
1858                 #TODO lot_id is now on quant and not on move, need to do something for this
1859                 #'lot_id': move.lot_id.id,
1860             }
1861             new_move = self.copy(cr, uid, move.id, default_val)
1862
1863             res += [new_move]
1864             product_obj = self.pool.get('product.product')
1865             for product in product_obj.browse(cr, uid, [move.product_id.id], context=context):
1866                 if move.picking_id:
1867                     uom = product.uom_id.name if product.uom_id else ''
1868                     message = _("%s %s %s has been <b>moved to</b> scrap.") % (quantity, uom, product.name)
1869                     move.picking_id.message_post(body=message)
1870
1871         self.action_done(cr, uid, res, context=context)
1872         return res
1873
1874     def action_consume(self, cr, uid, ids, quantity, location_id=False, context=None):
1875         """ Consumed product with specific quatity from specific source location
1876         @param cr: the database cursor
1877         @param uid: the user id
1878         @param ids: ids of stock move object to be consumed
1879         @param quantity : specify consume quantity
1880         @param location_id : specify source location
1881         @param context: context arguments
1882         @return: Consumed lines
1883         """
1884         #quantity should in MOVE UOM
1885         if context is None:
1886             context = {}
1887         if quantity <= 0:
1888             raise osv.except_osv(_('Warning!'), _('Please provide proper quantity.'))
1889         res = []
1890         for move in self.browse(cr, uid, ids, context=context):
1891             move_qty = move.product_qty
1892             if move_qty <= 0:
1893                 raise osv.except_osv(_('Error!'), _('Cannot consume a move with negative or zero quantity.'))
1894             quantity_rest = move.product_qty
1895             quantity_rest -= quantity
1896             uos_qty_rest = quantity_rest / move_qty * move.product_uos_qty
1897             if quantity_rest <= 0:
1898                 quantity_rest = 0
1899                 uos_qty_rest = 0
1900                 quantity = move.product_qty
1901
1902             uos_qty = quantity / move_qty * move.product_uos_qty
1903             if quantity_rest > 0:
1904                 default_val = {
1905                     'product_uom_qty': quantity,
1906                     'product_uos_qty': uos_qty,
1907                     'state': move.state,
1908                     'location_id': location_id or move.location_id.id,
1909                 }
1910                 current_move = self.copy(cr, uid, move.id, default_val)
1911                 res += [current_move]
1912                 update_val = {}
1913                 update_val['product_uom_qty'] = quantity_rest
1914                 update_val['product_uos_qty'] = uos_qty_rest
1915                 self.write(cr, uid, [move.id], update_val)
1916
1917             else:
1918                 quantity_rest = quantity
1919                 uos_qty_rest =  uos_qty
1920                 res += [move.id]
1921                 update_val = {
1922                         'product_uom_qty' : quantity_rest,
1923                         'product_uos_qty' : uos_qty_rest,
1924                         'location_id': location_id or move.location_id.id,
1925                 }
1926                 self.write(cr, uid, [move.id], update_val)
1927
1928         self.action_done(cr, uid, res, context=context)
1929         return res
1930
1931
1932
1933     def split(self, cr, uid, move, qty, context=None):
1934         """ 
1935             Splits qty from move move into a new move
1936         """
1937         if move.product_qty==qty:
1938             return move.id
1939         if (move.product_qty < qty) or (qty==0):
1940             return False
1941
1942         uom_obj = self.pool.get('product.uom')
1943         context = context or {}
1944
1945         uom_qty = uom_obj._compute_qty(cr, uid, move.product_id.uom_id.id, qty, move.product_uom.id)
1946         uos_qty = uom_qty * move.product_uos_qty / move.product_uom_qty
1947
1948         if move.state in ('done', 'cancel'):
1949             raise osv.except_osv(_('Error'), _('You cannot split a move done'))
1950
1951         defaults = {
1952             'product_uom_qty': uom_qty,
1953             'product_uos_qty': uos_qty,
1954             'state': move.state,
1955             'move_dest_id': False,
1956             'reserved_quant_ids': []
1957         }
1958         new_move = self.copy(cr, uid, move.id, defaults)
1959
1960         self.write(cr, uid, [move.id], {
1961             'product_uom_qty': move.product_uom_qty - uom_qty,
1962             'product_uos_qty': move.product_uos_qty - uos_qty,
1963 #             'reserved_quant_ids': [(6,0,[])]  SHOULD NOT CHANGE as it has been reserved already
1964         }, context=context)
1965         
1966         if move.move_dest_id and move.propagate:
1967             new_move_prop = self.split(cr, uid, move.move_dest_id, qty, context=context)
1968             self.write(cr, uid, [new_move], {'move_dest_id': new_move_prop}, context=context)
1969
1970         self.action_confirm(cr, uid, [new_move], context=context)
1971         return new_move
1972
1973 class stock_inventory(osv.osv):
1974     _name = "stock.inventory"
1975     _description = "Inventory"
1976
1977     def _get_move_ids_exist(self, cr, uid, ids, field_name, arg, context=None):
1978         res = {}
1979         for inv in self.browse(cr, uid, ids, context=context):
1980             res[inv.id] = False
1981             if inv.move_ids:
1982                 res[inv.id] = True
1983         return res
1984
1985     def _get_available_filters(self, cr, uid, context=None):
1986         """
1987            This function will return the list of filter allowed according to the options checked
1988            in 'Settings\Warehouse'.
1989
1990            :rtype: list of tuple
1991         """
1992         #default available choices
1993         res_filter = [('none', ' All products of a whole location'), ('product', 'One product only')]
1994         settings_obj = self.pool.get('stock.config.settings')
1995         config_ids = settings_obj.search(cr, uid, [], limit=1, order='id DESC', context=context)
1996         #If we don't have updated config until now, all fields are by default false and so should be not dipslayed
1997         if not config_ids:
1998             return res_filter
1999
2000         stock_settings = settings_obj.browse(cr, uid, config_ids[0], context=context)
2001         if stock_settings.group_stock_tracking_owner:
2002             res_filter.append(('owner', 'One owner only'))
2003             res_filter.append(('product_owner', 'One product for a specific owner'))
2004         if stock_settings.group_stock_production_lot:
2005             res_filter.append(('lot', 'One Lot/Serial Number'))
2006         if stock_settings.group_stock_tracking_lot:
2007             res_filter.append(('pack', 'A Pack'))
2008         return res_filter
2009
2010     _columns = {
2011         'name': fields.char('Inventory Reference', size=64, required=True, readonly=True, states={'draft': [('readonly', False)]}, help="Inventory Name."),
2012         'date': fields.datetime('Inventory Date', required=True, readonly=True, states={'draft': [('readonly', False)]}, help="Inventory Create Date."),
2013         'date_done': fields.datetime('Date done', help="Inventory Validation Date."),
2014         'line_ids': fields.one2many('stock.inventory.line', 'inventory_id', 'Inventories', readonly=False, states={'done': [('readonly', True)]}, help="Inventory Lines."),
2015         'move_ids': fields.one2many('stock.move', 'inventory_id', 'Created Moves', help="Inventory Moves."),
2016         'state': fields.selection([('draft', 'Draft'), ('cancel', 'Cancelled'), ('confirm', 'In Progress'), ('done', 'Validated')], 'Status', readonly=True, select=True),
2017         'company_id': fields.many2one('res.company', 'Company', required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
2018         'location_id': fields.many2one('stock.location', 'Location', required=True),
2019         'product_id': fields.many2one('product.product', 'Product', readonly=True, states={'draft': [('readonly', False)]}, help="Specify Product to focus your inventory on a particular Product."),
2020         'package_id': fields.many2one('stock.quant.package', 'Pack', readonly=True, states={'draft': [('readonly', False)]}, help="Specify Pack to focus your inventory on a particular Pack."),
2021         'partner_id': fields.many2one('res.partner', 'Owner', readonly=True, states={'draft': [('readonly', False)]}, help="Specify Owner to focus your inventory on a particular Owner."),
2022         'lot_id': fields.many2one('stock.production.lot', 'Lot/Serial Number', readonly=True, states={'draft': [('readonly', False)]}, help="Specify Lot/Serial Number to focus your inventory on a particular Lot/Serial Number."),
2023         'move_ids_exist': fields.function(_get_move_ids_exist, type='boolean', string=' Stock Move Exists?', help='technical field for attrs in view'),
2024         'filter': fields.selection(_get_available_filters, 'Selection Filter'),
2025     }
2026
2027     def _default_stock_location(self, cr, uid, context=None):
2028         try:
2029             warehouse = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'warehouse0')
2030             return warehouse.lot_stock_id.id
2031         except:
2032             return False
2033
2034     _defaults = {
2035         'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
2036         'state': 'draft',
2037         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.inventory', context=c),
2038         'location_id': _default_stock_location,
2039     }
2040
2041     def set_checked_qty(self, cr, uid, ids, context=None):
2042         inventory = self.browse(cr, uid, ids[0], context=context)
2043         line_ids = [line.id for line in inventory.line_ids]
2044         self.pool.get('stock.inventory.line').write(cr, uid, line_ids, {'product_qty': 0})
2045         return True
2046
2047     def copy(self, cr, uid, id, default=None, context=None):
2048         if default is None:
2049             default = {}
2050         default = default.copy()
2051         default.update({'move_ids': [], 'date_done': False})
2052         return super(stock_inventory, self).copy(cr, uid, id, default, context=context)
2053
2054     def _inventory_line_hook(self, cr, uid, inventory_line, move_vals):
2055         """ Creates a stock move from an inventory line
2056         @param inventory_line:
2057         @param move_vals:
2058         @return:
2059         """
2060         return self.pool.get('stock.move').create(cr, uid, move_vals)
2061
2062     def action_done(self, cr, uid, ids, context=None):
2063         """ Finish the inventory
2064         @return: True
2065         """
2066         if context is None:
2067             context = {}
2068         move_obj = self.pool.get('stock.move')
2069         for inv in self.browse(cr, uid, ids, context=context):
2070             if not inv.move_ids:
2071                 self.action_check(cr, uid, [inv.id], context=context)
2072             inv.refresh()
2073             #the action_done on stock_move has to be done in 2 steps:
2074             #first, we start moving the products from stock to inventory loss
2075             move_obj.action_done(cr, uid, [x.id for x in inv.move_ids if x.location_id.usage == 'internal'], context=context)
2076             #then, we move from inventory loss. This 2 steps process is needed because some moved quant may need to be put again in stock
2077             move_obj.action_done(cr, uid, [x.id for x in inv.move_ids if x.location_id.usage != 'internal'], context=context)
2078             self.write(cr, uid, [inv.id], {'state': 'done', 'date_done': time.strftime('%Y-%m-%d %H:%M:%S')}, context=context)
2079         return True
2080
2081     def _create_stock_move(self, cr, uid, inventory, todo_line, context=None):
2082         stock_move_obj = self.pool.get('stock.move')
2083         product_obj = self.pool.get('product.product')
2084         inventory_location_id = product_obj.browse(cr, uid, todo_line['product_id'], context=context).property_stock_inventory.id
2085         vals = {
2086             'name': _('INV:') + (inventory.name or ''),
2087             'product_id': todo_line['product_id'],
2088             'product_uom': todo_line['product_uom_id'],
2089             'date': inventory.date,
2090             'company_id': inventory.company_id.id,
2091             'inventory_id': inventory.id,
2092             'state': 'assigned',
2093             'restrict_lot_id': todo_line.get('prod_lot_id'),
2094             'restrict_partner_id': todo_line.get('partner_id'),
2095          }
2096
2097         if todo_line['product_qty'] < 0:
2098             #found more than expected
2099             vals['location_id'] = inventory_location_id
2100             vals['location_dest_id'] = todo_line['location_id']
2101             vals['product_uom_qty'] = -todo_line['product_qty']
2102         else:
2103             #found less than expected
2104             vals['location_id'] = todo_line['location_id']
2105             vals['location_dest_id'] = inventory_location_id
2106             vals['product_uom_qty'] = todo_line['product_qty']
2107         return stock_move_obj.create(cr, uid, vals, context=context)
2108
2109     def action_check(self, cr, uid, ids, context=None):
2110         """ Checks the inventory and computes the stock move to do
2111         @return: True
2112         """
2113         inventory_line_obj = self.pool.get('stock.inventory.line')
2114         stock_move_obj = self.pool.get('stock.move')
2115         for inventory in self.browse(cr, uid, ids, context=context):
2116             #first remove the existing stock moves linked to this inventory
2117             move_ids = [move.id for move in inventory.move_ids]
2118             stock_move_obj.unlink(cr, uid, move_ids, context=context)
2119             #compute what should be in the inventory lines
2120             theorical_lines = self._get_inventory_lines(cr, uid, inventory, context=context)
2121             for line in inventory.line_ids:
2122                 #compare the inventory lines to the theorical ones and store the diff in theorical_lines
2123                 inventory_line_obj._resolve_inventory_line(cr, uid, line, theorical_lines, context=context)
2124             #each theorical_lines where product_qty is not 0 is a difference for which we need to create a stock move
2125             for todo_line in theorical_lines:
2126                 if todo_line['product_qty'] != 0:
2127                     self._create_stock_move(cr, uid, inventory, todo_line, context=context)
2128
2129     def action_cancel_draft(self, cr, uid, ids, context=None):
2130         """ Cancels the stock move and change inventory state to draft.
2131         @return: True
2132         """
2133         for inv in self.browse(cr, uid, ids, context=context):
2134             self.pool.get('stock.move').action_cancel(cr, uid, [x.id for x in inv.move_ids], context=context)
2135             self.write(cr, uid, [inv.id], {'state': 'draft'}, context=context)
2136         return True
2137
2138     def action_cancel_inventory(self, cr, uid, ids, context=None):
2139         #TODO test
2140         self.action_cancel_draft(cr, uid, ids, context=context)
2141
2142     def prepare_inventory(self, cr, uid, ids, context=None):
2143         inventory_line_obj = self.pool.get('stock.inventory.line')
2144         for inventory in self.browse(cr, uid, ids, context=context):
2145             #clean the existing inventory lines before redoing an inventory proposal
2146             line_ids = [line.id for line in inventory.line_ids]
2147             inventory_line_obj.unlink(cr, uid, line_ids, context=context)
2148             #compute the inventory lines and create them
2149             vals = self._get_inventory_lines(cr, uid, inventory, context=context)
2150             for product_line in vals:
2151                 inventory_line_obj.create(cr, uid, product_line, context=context)
2152         return self.write(cr, uid, ids, {'state': 'confirm'})
2153
2154     def _get_inventory_lines(self, cr, uid, inventory, context=None):
2155         location_obj = self.pool.get('stock.location')
2156         product_obj = self.pool.get('product.product')
2157         location_ids = location_obj.search(cr, uid, [('id', 'child_of', [inventory.location_id.id])], context=context)
2158         domain = ' location_id in %s'
2159         args = (tuple(location_ids),)
2160         if inventory.partner_id:
2161             domain += ' and owner_id = %s'
2162             args += (inventory.partner_id.id,)
2163         if inventory.lot_id:
2164             domain += ' and lot_id = %s'
2165             args += (inventory.lot_id.id,)
2166         if inventory.product_id:
2167             domain += 'and product_id = %s'
2168             args += (inventory.product_id.id,)
2169         if inventory.package_id:
2170             domain += ' and package_id = %s'
2171             args += (inventory.package_id.id,)
2172         cr.execute('''
2173            SELECT product_id, sum(qty) as product_qty, location_id, lot_id as prod_lot_id, package_id, owner_id as partner_id
2174            FROM stock_quant WHERE''' + domain + '''
2175            GROUP BY product_id, location_id, lot_id, package_id, partner_id
2176         ''', args)
2177         vals = []
2178         for product_line in cr.dictfetchall():
2179             #replace the None the dictionary by False, because falsy values are tested later on
2180             for key, value in product_line.items():
2181                 if not value:
2182                     product_line[key] = False
2183             product_line['inventory_id'] = inventory.id
2184             product_line['th_qty'] = product_line['product_qty']
2185             if product_line['product_id']:
2186                 product = product_obj.browse(cr, uid, product_line['product_id'], context=context)
2187                 product_line['product_uom_id'] = product.uom_id.id
2188             vals.append(product_line)
2189         return vals
2190
2191 class stock_inventory_line(osv.osv):
2192     _name = "stock.inventory.line"
2193     _description = "Inventory Line"
2194     _rec_name = "inventory_id"
2195     _columns = {
2196         'inventory_id': fields.many2one('stock.inventory', 'Inventory', ondelete='cascade', select=True),
2197         'location_id': fields.many2one('stock.location', 'Location', required=True, select=True),
2198         'product_id': fields.many2one('product.product', 'Product', required=True, select=True),
2199         'package_id': fields.many2one('stock.quant.package', 'Pack', select=True),
2200         'product_uom_id': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
2201         'product_qty': fields.float('Checked Quantity', digits_compute=dp.get_precision('Product Unit of Measure')),
2202         'company_id': fields.related('inventory_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, select=True, readonly=True),
2203         'prod_lot_id': fields.many2one('stock.production.lot', 'Serial Number', domain="[('product_id','=',product_id)]"),
2204         'state': fields.related('inventory_id', 'state', type='char', string='Status', readonly=True),
2205         'th_qty': fields.float('Theoretical Quantity', readonly=True),
2206         'partner_id': fields.many2one('res.partner', 'Owner'),
2207     }
2208
2209     _defaults = {
2210         'product_qty': 1,
2211     }
2212
2213     def _resolve_inventory_line(self, cr, uid, inventory_line, theorical_lines, context=None):
2214         #TODO : package_id management !
2215         found = False
2216         uom_obj = self.pool.get('product.uom')
2217         for th_line in theorical_lines:
2218             #We try to match the inventory line with a theorical line with same product, lot, location and owner
2219             if th_line['location_id'] == inventory_line.location_id.id and th_line['product_id'] == inventory_line.product_id.id and th_line['prod_lot_id'] == inventory_line.prod_lot_id.id and th_line['partner_id'] == inventory_line.partner_id.id:
2220                 uom_reference = inventory_line.product_id.uom_id
2221                 real_qty = uom_obj._compute_qty_obj(cr, uid, inventory_line.product_uom_id, inventory_line.product_qty, uom_reference)
2222                 th_line['product_qty'] -= real_qty
2223                 found = True
2224                 break
2225         #if it was still not found, we add it to the theorical lines so that it will create a stock move for it
2226         if not found:
2227             vals = {
2228                 'inventory_id': inventory_line.inventory_id.id,
2229                 'location_id': inventory_line.location_id.id,
2230                 'product_id': inventory_line.product_id.id,
2231                 'product_uom_id': inventory_line.product_id.uom_id.id,
2232                 'product_qty': -inventory_line.product_qty,
2233                 'prod_lot_id': inventory_line.prod_lot_id.id,
2234                 'partner_id': inventory_line.partner_id.id,
2235             }
2236             theorical_lines.append(vals)
2237
2238     def on_change_product_id(self, cr, uid, ids, location_id, product, uom=False, owner_id=False, lot_id=False, package_id=False, context=None):
2239         """ Changes UoM and name if product_id changes.
2240         @param location_id: Location id
2241         @param product: Changed product_id
2242         @param uom: UoM product
2243         @return:  Dictionary of changed values
2244         """
2245         context = context or {}
2246         if not product:
2247             return {'value': {'product_qty': 0.0, 'product_uom_id': False}}
2248         uom_obj = self.pool.get('product.uom')
2249         ctx = context.copy()
2250         ctx['location'] = location_id
2251         ctx['lot_id'] = lot_id
2252         ctx['owner_id'] = owner_id
2253         ctx['package_id'] = package_id
2254         obj_product = self.pool.get('product.product').browse(cr, uid, product, context=ctx)
2255         th_qty = obj_product.qty_available
2256         if uom and uom != obj_product.uom_id.id:
2257             uom_record = uom_obj.browse(cr, uid, uom, context=context)
2258             th_qty = uom_obj._compute_qty_obj(cr, uid, obj_product.uom_id, th_qty, uom_record)
2259         return {'value': {'th_qty': th_qty, 'product_uom_id': uom or obj_product.uom_id.id}}
2260
2261
2262 #----------------------------------------------------------
2263 # Stock Warehouse
2264 #----------------------------------------------------------
2265 class stock_warehouse(osv.osv):
2266     _name = "stock.warehouse"
2267     _description = "Warehouse"
2268
2269     _columns = {
2270         'name': fields.char('Name', size=128, required=True, select=True),
2271         'company_id': fields.many2one('res.company', 'Company', required=True, select=True),
2272         'partner_id': fields.many2one('res.partner', 'Address'),
2273         'view_location_id': fields.many2one('stock.location', 'View Location', required=True, domain=[('usage', '=', 'view')]),
2274         'lot_stock_id': fields.many2one('stock.location', 'Location Stock', required=True, domain=[('usage', '=', 'internal')]),
2275         'code': fields.char('Short Name', size=5, required=True, help="Short name used to identify your warehouse"),
2276         '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'),
2277         'reception_steps': fields.selection([
2278             ('one_step', 'Receive goods directly in stock (1 step)'),
2279             ('two_steps', 'Unload in input location then go to stock (2 steps)'),
2280             ('three_steps', 'Unload in input location, go through a quality control before being admitted in stock (3 steps)')], 'Incoming Shipments', required=True),
2281         'delivery_steps': fields.selection([
2282             ('ship_only', 'Ship directly from stock (Ship only)'),
2283             ('pick_ship', 'Bring goods to output location before shipping (Pick + Ship)'),
2284             ('pick_pack_ship', 'Make packages into a dedicated location, then bring them to the output location for shipping (Pick + Pack + Ship)')], 'Outgoing Shippings', required=True),
2285         'wh_input_stock_loc_id': fields.many2one('stock.location', 'Input Location'),
2286         'wh_qc_stock_loc_id': fields.many2one('stock.location', 'Quality Control Location'),
2287         'wh_output_stock_loc_id': fields.many2one('stock.location', 'Output Location'),
2288         'wh_pack_stock_loc_id': fields.many2one('stock.location', 'Packing Location'),
2289         'mto_pull_id': fields.many2one('procurement.rule', 'MTO rule'),
2290         'pick_type_id': fields.many2one('stock.picking.type', 'Pick Type'),
2291         'pack_type_id': fields.many2one('stock.picking.type', 'Pack Type'),
2292         'out_type_id': fields.many2one('stock.picking.type', 'Out Type'),
2293         'in_type_id': fields.many2one('stock.picking.type', 'In Type'),
2294         'int_type_id': fields.many2one('stock.picking.type', 'Internal Type'),
2295         'crossdock_route_id': fields.many2one('stock.location.route', 'Crossdock Route'),
2296         'reception_route_id': fields.many2one('stock.location.route', 'Reception Route'),
2297         'delivery_route_id': fields.many2one('stock.location.route', 'Delivery Route'),
2298         'resupply_from_wh': fields.boolean('Resupply From Other Warehouses'),
2299         'resupply_wh_ids': fields.many2many('stock.warehouse', 'stock_wh_resupply_table', 'supplied_wh_id', 'supplier_wh_id', 'Resupply Warehouses'),
2300         'resupply_route_ids': fields.one2many('stock.location.route', 'supplied_wh_id', 'Resupply Routes'),
2301         'default_resupply_wh_id': fields.many2one('stock.warehouse', 'Default Resupply Warehouse'),
2302     }
2303
2304     def onchange_filter_default_resupply_wh_id(self, cr, uid, ids, default_resupply_wh_id, resupply_wh_ids, context=None):
2305         resupply_wh_ids = set([x['id'] for x in (self.resolve_2many_commands(cr, uid, 'resupply_wh_ids', resupply_wh_ids, ['id']))])
2306         if default_resupply_wh_id: #If we are removing the default resupply, we don't have default_resupply_wh_id 
2307             resupply_wh_ids.add(default_resupply_wh_id)
2308         resupply_wh_ids = list(resupply_wh_ids)        
2309         return {'value': {'resupply_wh_ids': resupply_wh_ids}}
2310
2311     def _get_inter_wh_location(self, cr, uid, warehouse, context=None):
2312         ''' returns a tuple made of the browse record of customer location and the browse record of supplier location'''
2313         data_obj = self.pool.get('ir.model.data')
2314         try:
2315             inter_wh_loc = data_obj.get_object_reference(cr, uid, 'stock', 'stock_location_inter_wh')[1]
2316         except:
2317             inter_wh_loc = False
2318         return inter_wh_loc
2319
2320     def _get_all_products_to_resupply(self, cr, uid, warehouse, context=None):
2321         return self.pool.get('product.product').search(cr, uid, [], context=context)
2322
2323     def _assign_route_on_products(self, cr, uid, warehouse, inter_wh_route_id, context=None):
2324         product_ids = self._get_all_products_to_resupply(cr, uid, warehouse, context=context)
2325         self.pool.get('product.product').write(cr, uid, product_ids, {'route_ids': [(4, inter_wh_route_id)]}, context=context)
2326
2327     def _unassign_route_on_products(self, cr, uid, warehouse, inter_wh_route_id, context=None):
2328         product_ids = self._get_all_products_to_resupply(cr, uid, warehouse, context=context)
2329         self.pool.get('product.product').write(cr, uid, product_ids, {'route_ids': [(3, inter_wh_route_id)]}, context=context)
2330
2331     def _get_inter_wh_route(self, cr, uid, warehouse, wh, context=None):
2332         return {
2333             'name': _('%s: Supply Product from %s') % (warehouse.name, wh.name),
2334             'warehouse_selectable': False,
2335             'product_selectable': True,
2336             'product_categ_selectable': True,
2337             'supplied_wh_id': warehouse.id,
2338             'supplier_wh_id': wh.id,
2339         }
2340
2341     def _create_resupply_routes(self, cr, uid, warehouse, supplier_warehouses, default_resupply_wh, context=None):
2342         location_obj = self.pool.get('stock.location')
2343         route_obj = self.pool.get('stock.location.route')
2344         pull_obj = self.pool.get('procurement.rule')
2345         #create route selectable on the product to resupply the warehouse from another one
2346         inter_wh_location_id = self._get_inter_wh_location(cr, uid, warehouse, context=context)
2347         if inter_wh_location_id:
2348             input_loc = warehouse.wh_input_stock_loc_id
2349             if warehouse.reception_steps == 'one_step':
2350                 input_loc = warehouse.lot_stock_id
2351             inter_wh_location = location_obj.browse(cr, uid, inter_wh_location_id, context=context)
2352             for wh in supplier_warehouses:
2353                 output_loc = wh.wh_output_stock_loc_id
2354                 if wh.delivery_steps == 'ship_only':
2355                     output_loc = wh.lot_stock_id
2356                 inter_wh_route_vals = self._get_inter_wh_route(cr, uid, warehouse, wh, context=context)
2357                 inter_wh_route_id = route_obj.create(cr, uid, vals=inter_wh_route_vals, context=context)
2358                 values = [(output_loc, inter_wh_location, wh.out_type_id.id, wh), (inter_wh_location, input_loc, warehouse.in_type_id.id, warehouse)]
2359                 pull_rules_list = self._get_supply_pull_rules(cr, uid, warehouse, values, inter_wh_route_id, context=context)
2360                 for pull_rule in pull_rules_list:
2361                     pull_obj.create(cr, uid, vals=pull_rule, context=context)
2362                 #if the warehouse is also set as default resupply method, assign this route automatically to all product
2363                 if default_resupply_wh and default_resupply_wh.id == wh.id:
2364                     self._assign_route_on_products(cr, uid, warehouse, inter_wh_route_id, context=context)
2365                 #finally, save the route on the warehouse
2366                 self.write(cr, uid, [warehouse.id], {'route_ids': [(4, inter_wh_route_id)]}, context=context)
2367
2368     def _default_stock_id(self, cr, uid, context=None):
2369         #lot_input_stock = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'stock_location_stock')
2370         try:
2371             warehouse = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'warehouse0')
2372             return warehouse.lot_stock_id.id
2373         except:
2374             return False
2375
2376     _defaults = {
2377         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.inventory', context=c),
2378         'lot_stock_id': _default_stock_id,
2379         'reception_steps': 'one_step',
2380         'delivery_steps': 'ship_only',
2381     }
2382     _sql_constraints = [
2383         ('warehouse_name_uniq', 'unique(name, company_id)', 'The name of the warehouse must be unique per company!'),
2384         ('warehouse_code_uniq', 'unique(code, company_id)', 'The code of the warehouse must be unique per company!'),
2385         ('default_resupply_wh_diff', 'check (id != default_resupply_wh_id)', 'The default resupply warehouse should be different that the warehouse itself!'),
2386     ]
2387
2388     def _get_partner_locations(self, cr, uid, ids, context=None):
2389         ''' returns a tuple made of the browse record of customer location and the browse record of supplier location'''
2390         data_obj = self.pool.get('ir.model.data')
2391         location_obj = self.pool.get('stock.location')
2392         try:
2393             customer_loc = data_obj.get_object_reference(cr, uid, 'stock', 'stock_location_customers')[1]
2394             supplier_loc = data_obj.get_object_reference(cr, uid, 'stock', 'stock_location_suppliers')[1]
2395         except:
2396             customer_loc = location_obj.search(cr, uid, [('usage', '=', 'customer')], context=context)
2397             customer_loc = customer_loc and customer_loc[0] or False
2398             supplier_loc = location_obj.search(cr, uid, [('usage', '=', 'supplier')], context=context)
2399             supplier_loc = supplier_loc and supplier_loc[0] or False
2400         if not (customer_loc and supplier_loc):
2401             raise osv.except_osv(_('Error!'), _('Can\'t find any customer or supplier location.'))
2402         return location_obj.browse(cr, uid, [customer_loc, supplier_loc], context=context)
2403
2404     def switch_location(self, cr, uid, ids, warehouse, new_reception_step=False, new_delivery_step=False, context=None):
2405         location_obj = self.pool.get('stock.location')
2406
2407         new_reception_step = new_reception_step or warehouse.reception_steps
2408         new_delivery_step = new_delivery_step or warehouse.delivery_steps
2409         if warehouse.reception_steps != new_reception_step:
2410             location_obj.write(cr, uid, [warehouse.wh_input_stock_loc_id.id, warehouse.wh_qc_stock_loc_id.id], {'active': False}, context=context)
2411             if new_reception_step != 'one_step':
2412                 location_obj.write(cr, uid, warehouse.wh_input_stock_loc_id.id, {'active': True}, context=context)
2413             if new_reception_step == 'three_steps':
2414                 location_obj.write(cr, uid, warehouse.wh_qc_stock_loc_id.id, {'active': True}, context=context)
2415
2416         if warehouse.delivery_steps != new_delivery_step:
2417             location_obj.write(cr, uid, [warehouse.wh_output_stock_loc_id.id, warehouse.wh_pack_stock_loc_id.id], {'active': False}, context=context)
2418             if new_delivery_step != 'ship_only':
2419                 location_obj.write(cr, uid, warehouse.wh_output_stock_loc_id.id, {'active': True}, context=context)
2420             if new_delivery_step == 'pick_pack_ship':
2421                 location_obj.write(cr, uid, warehouse.wh_pack_stock_loc_id.id, {'active': True}, context=context)
2422         return True
2423
2424     def _get_reception_delivery_route(self, cr, uid, warehouse, route_name, context=None):
2425         return {
2426             'name': self._format_routename(cr, uid, warehouse, route_name, context=context),
2427             'product_categ_selectable': True,
2428             'product_selectable': False,
2429             'sequence': 10,
2430         }
2431
2432     def _get_supply_pull_rules(self, cr, uid, supplied_warehouse, values, new_route_id, context=None):
2433         pull_rules_list = []
2434         for from_loc, dest_loc, pick_type_id, warehouse in values:
2435             pull_rules_list.append({
2436                 'name': self._format_rulename(cr, uid, warehouse, from_loc, dest_loc, context=context),
2437                 'location_src_id': from_loc.id,
2438                 'location_id': dest_loc.id,
2439                 'route_id': new_route_id,
2440                 'action': 'move',
2441                 'picking_type_id': pick_type_id,
2442                 'procure_method': 'make_to_order',
2443                 'warehouse_id': supplied_warehouse.id,
2444                 'propagate_warehouse_id': warehouse.id,
2445             })
2446         return pull_rules_list
2447
2448     def _get_push_pull_rules(self, cr, uid, warehouse, active, values, new_route_id, context=None):
2449         first_rule = True
2450         push_rules_list = []
2451         pull_rules_list = []
2452         for from_loc, dest_loc, pick_type_id in values:
2453             push_rules_list.append({
2454                 'name': self._format_rulename(cr, uid, warehouse, from_loc, dest_loc, context=context),
2455                 'location_from_id': from_loc.id,
2456                 'location_dest_id': dest_loc.id,
2457                 'route_id': new_route_id,
2458                 'auto': 'manual',
2459                 'picking_type_id': pick_type_id,
2460                 'active': active,
2461                 'warehouse_id': warehouse.id,
2462             })
2463             pull_rules_list.append({
2464                 'name': self._format_rulename(cr, uid, warehouse, from_loc, dest_loc, context=context),
2465                 'location_src_id': from_loc.id,
2466                 'location_id': dest_loc.id,
2467                 'route_id': new_route_id,
2468                 'action': 'move',
2469                 'picking_type_id': pick_type_id,
2470                 'procure_method': first_rule is True and 'make_to_stock' or 'make_to_order',
2471                 'active': active,
2472                 'warehouse_id': warehouse.id,
2473             })
2474             first_rule = False
2475         return push_rules_list, pull_rules_list
2476
2477     def _get_mto_pull_rule(self, cr, uid, warehouse, values, context=None):
2478         route_obj = self.pool.get('stock.location.route')
2479         data_obj = self.pool.get('ir.model.data')
2480         try:
2481             mto_route_id = data_obj.get_object_reference(cr, uid, 'stock', 'route_warehouse0_mto')[1]
2482         except:
2483             mto_route_id = route_obj.search(cr, uid, [('name', 'like', _('MTO'))], context=context)
2484             mto_route_id = mto_route_id and mto_route_id[0] or False
2485         if not mto_route_id:
2486             raise osv.except_osv(_('Error!'), _('Can\'t find any generic MTO route.'))
2487
2488         from_loc, dest_loc, pick_type_id = values[0]
2489         return {
2490             'name': self._format_rulename(cr, uid, warehouse, from_loc, dest_loc, context=context) + _(' MTO'),
2491             'location_src_id': from_loc.id,
2492             'location_id': dest_loc.id,
2493             'route_id': mto_route_id,
2494             'action': 'move',
2495             'picking_type_id': pick_type_id,
2496             'procure_method': 'make_to_order',
2497             'active': True,
2498             'warehouse_id': warehouse.id,
2499         }
2500
2501     def _get_crossdock_route(self, cr, uid, warehouse, route_name, context=None):
2502         return {
2503             'name': self._format_routename(cr, uid, warehouse, route_name, context=context),
2504             'warehouse_selectable': False,
2505             'product_selectable': True,
2506             'product_categ_selectable': True,
2507             'active': warehouse.delivery_steps != 'ship_only' and warehouse.reception_steps != 'one_step',
2508             'sequence': 20,
2509         }
2510
2511     def create_routes(self, cr, uid, ids, warehouse, context=None):
2512         wh_route_ids = []
2513         route_obj = self.pool.get('stock.location.route')
2514         pull_obj = self.pool.get('procurement.rule')
2515         push_obj = self.pool.get('stock.location.path')
2516         routes_dict = self.get_routes_dict(cr, uid, ids, warehouse, context=context)
2517         #create reception route and rules
2518         route_name, values = routes_dict[warehouse.reception_steps]
2519         route_vals = self._get_reception_delivery_route(cr, uid, warehouse, route_name, context=context)
2520         reception_route_id = route_obj.create(cr, uid, route_vals, context=context)
2521         wh_route_ids.append((4, reception_route_id))
2522         push_rules_list, pull_rules_list = self._get_push_pull_rules(cr, uid, warehouse, True, values, reception_route_id, context=context)
2523         #create the push/pull rules
2524         for push_rule in push_rules_list:
2525             push_obj.create(cr, uid, vals=push_rule, context=context)
2526         for pull_rule in pull_rules_list:
2527             #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
2528             pull_rule['procure_method'] = 'make_to_order'
2529             pull_obj.create(cr, uid, vals=pull_rule, context=context)
2530
2531         #create MTS route and pull rules for delivery a specific route MTO to be set on the product
2532         route_name, values = routes_dict[warehouse.delivery_steps]
2533         route_vals = self._get_reception_delivery_route(cr, uid, warehouse, route_name, context=context)
2534         #create the route and its pull rules
2535         delivery_route_id = route_obj.create(cr, uid, route_vals, context=context)
2536         wh_route_ids.append((4, delivery_route_id))
2537         dummy, pull_rules_list = self._get_push_pull_rules(cr, uid, warehouse, True, values, delivery_route_id, context=context)
2538         for pull_rule in pull_rules_list:
2539             pull_obj.create(cr, uid, vals=pull_rule, context=context)
2540         #create MTO pull rule and link it to the generic MTO route
2541         mto_pull_vals = self._get_mto_pull_rule(cr, uid, warehouse, values, context=context)
2542         mto_pull_id = pull_obj.create(cr, uid, mto_pull_vals, context=context)
2543
2544         #create a route for cross dock operations, that can be set on products and product categories
2545         route_name, values = routes_dict['crossdock']
2546         crossdock_route_vals = self._get_crossdock_route(cr, uid, warehouse, route_name, context=context)
2547         crossdock_route_id = route_obj.create(cr, uid, vals=crossdock_route_vals, context=context)
2548         wh_route_ids.append((4, crossdock_route_id))
2549         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)
2550         for pull_rule in pull_rules_list:
2551             pull_obj.create(cr, uid, vals=pull_rule, context=context)
2552
2553         #create route selectable on the product to resupply the warehouse from another one
2554         self._create_resupply_routes(cr, uid, warehouse, warehouse.resupply_wh_ids, warehouse.default_resupply_wh_id, context=context)
2555
2556         #return routes and mto pull rule to store on the warehouse
2557         return {
2558             'route_ids': wh_route_ids,
2559             'mto_pull_id': mto_pull_id,
2560             'reception_route_id': reception_route_id,
2561             'delivery_route_id': delivery_route_id,
2562             'crossdock_route_id': crossdock_route_id,
2563         }
2564
2565     def change_route(self, cr, uid, ids, warehouse, new_reception_step=False, new_delivery_step=False, context=None):
2566         picking_type_obj = self.pool.get('stock.picking.type')
2567         pull_obj = self.pool.get('procurement.rule')
2568         push_obj = self.pool.get('stock.location.path')
2569         route_obj = self.pool.get('stock.location.route')
2570         new_reception_step = new_reception_step or warehouse.reception_steps
2571         new_delivery_step = new_delivery_step or warehouse.delivery_steps
2572
2573         #change the default source and destination location and (de)activate picking types
2574         input_loc = warehouse.wh_input_stock_loc_id
2575         if new_reception_step == 'one_step':
2576             input_loc = warehouse.lot_stock_id
2577         output_loc = warehouse.wh_output_stock_loc_id
2578         if new_delivery_step == 'ship_only':
2579             output_loc = warehouse.lot_stock_id
2580         picking_type_obj.write(cr, uid, warehouse.in_type_id.id, {'default_location_dest_id': input_loc.id}, context=context)
2581         picking_type_obj.write(cr, uid, warehouse.out_type_id.id, {'default_location_src_id': output_loc.id}, context=context)
2582         picking_type_obj.write(cr, uid, warehouse.int_type_id.id, {'active': new_reception_step != 'one_step'}, context=context)
2583         picking_type_obj.write(cr, uid, warehouse.pick_type_id.id, {'active': new_delivery_step != 'ship_only'}, context=context)
2584         picking_type_obj.write(cr, uid, warehouse.pack_type_id.id, {'active': new_delivery_step == 'pick_pack_ship'}, context=context)
2585
2586         routes_dict = self.get_routes_dict(cr, uid, ids, warehouse, context=context)
2587         #update delivery route and rules: unlink the existing rules of the warehouse delivery route and recreate it
2588         pull_obj.unlink(cr, uid, [pu.id for pu in warehouse.delivery_route_id.pull_ids], context=context)
2589         route_name, values = routes_dict[new_delivery_step]
2590         route_obj.write(cr, uid, warehouse.delivery_route_id.id, {'name': self._format_routename(cr, uid, warehouse, route_name, context=context)}, context=context)
2591         dummy, pull_rules_list = self._get_push_pull_rules(cr, uid, warehouse, True, values, warehouse.delivery_route_id.id, context=context)
2592         #create the pull rules
2593         for pull_rule in pull_rules_list:
2594             pull_obj.create(cr, uid, vals=pull_rule, context=context)
2595
2596         #update reception route and rules: unlink the existing rules of the warehouse reception route and recreate it
2597         pull_obj.unlink(cr, uid, [pu.id for pu in warehouse.reception_route_id.pull_ids], context=context)
2598         push_obj.unlink(cr, uid, [pu.id for pu in warehouse.reception_route_id.push_ids], context=context)
2599         route_name, values = routes_dict[new_reception_step]
2600         route_obj.write(cr, uid, warehouse.reception_route_id.id, {'name': self._format_routename(cr, uid, warehouse, route_name, context=context)}, context=context)
2601         push_rules_list, pull_rules_list = self._get_push_pull_rules(cr, uid, warehouse, True, values, warehouse.reception_route_id.id, context=context)
2602         #create the push/pull rules
2603         for push_rule in push_rules_list:
2604             push_obj.create(cr, uid, vals=push_rule, context=context)
2605         for pull_rule in pull_rules_list:
2606             #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
2607             pull_rule['procure_method'] = 'make_to_order'
2608             pull_obj.create(cr, uid, vals=pull_rule, context=context)
2609
2610         route_obj.write(cr, uid, warehouse.crossdock_route_id.id, {'active': new_reception_step != 'one_step' and new_delivery_step != 'ship_only'}, context=context)
2611
2612         #change MTO rule
2613         dummy, values = routes_dict[new_delivery_step]
2614         mto_pull_vals = self._get_mto_pull_rule(cr, uid, warehouse, values, context=context)
2615         pull_obj.write(cr, uid, warehouse.mto_pull_id.id, mto_pull_vals, context=context)
2616         return True
2617
2618     def create(self, cr, uid, vals, context=None):
2619         if context is None:
2620             context = {}
2621         if vals is None:
2622             vals = {}
2623         data_obj = self.pool.get('ir.model.data')
2624         seq_obj = self.pool.get('ir.sequence')
2625         picking_type_obj = self.pool.get('stock.picking.type')
2626         location_obj = self.pool.get('stock.location')
2627
2628         #create view location for warehouse
2629         wh_loc_id = location_obj.create(cr, uid, {
2630                 'name': _(vals.get('name')),
2631                 'usage': 'view',
2632                 'location_id': data_obj.get_object_reference(cr, uid, 'stock', 'stock_location_locations')[1]
2633             }, context=context)
2634         vals['view_location_id'] = wh_loc_id
2635         #create all location
2636         def_values = self.default_get(cr, uid, {'reception_steps', 'delivery_steps'})
2637         reception_steps = vals.get('reception_steps',  def_values['reception_steps'])
2638         delivery_steps = vals.get('delivery_steps', def_values['delivery_steps'])
2639         context_with_inactive = context.copy()
2640         context_with_inactive['active_test'] = False
2641         sub_locations = [
2642             {'name': _('Stock'), 'active': True, 'field': 'lot_stock_id'},
2643             {'name': _('Input'), 'active': reception_steps != 'one_step', 'field': 'wh_input_stock_loc_id'},
2644             {'name': _('Quality Control'), 'active': reception_steps == 'three_steps', 'field': 'wh_qc_stock_loc_id'},
2645             {'name': _('Output'), 'active': delivery_steps != 'ship_only', 'field': 'wh_output_stock_loc_id'},
2646             {'name': _('Packing Zone'), 'active': delivery_steps == 'pick_pack_ship', 'field': 'wh_pack_stock_loc_id'},
2647         ]
2648         for values in sub_locations:
2649             location_id = location_obj.create(cr, uid, {
2650                 'name': values['name'],
2651                 'usage': 'internal',
2652                 'location_id': wh_loc_id,
2653                 'active': values['active'],
2654             }, context=context_with_inactive)
2655             vals[values['field']] = location_id
2656
2657         #create new sequences
2658         in_seq_id = seq_obj.create(cr, SUPERUSER_ID, values={'name': vals.get('name', '') + _(' Sequence in'), 'prefix': vals.get('code', '') + '\IN\\', 'padding': 5}, context=context)
2659         out_seq_id = seq_obj.create(cr, SUPERUSER_ID, values={'name': vals.get('name', '') + _(' Sequence out'), 'prefix': vals.get('code', '') + '\OUT\\', 'padding': 5}, context=context)
2660         pack_seq_id = seq_obj.create(cr, SUPERUSER_ID, values={'name': vals.get('name', '') + _(' Sequence packing'), 'prefix': vals.get('code', '') + '\PACK\\', 'padding': 5}, context=context)
2661         pick_seq_id = seq_obj.create(cr, SUPERUSER_ID, values={'name': vals.get('name', '') + _(' Sequence picking'), 'prefix': vals.get('code', '') + '\PICK\\', 'padding': 5}, context=context)
2662         int_seq_id = seq_obj.create(cr, SUPERUSER_ID, values={'name': vals.get('name', '') + _(' Sequence internal'), 'prefix': vals.get('code', '') + '\INT\\', 'padding': 5}, context=context)
2663
2664         #create WH
2665         new_id = super(stock_warehouse, self).create(cr, uid, vals=vals, context=context)
2666
2667         warehouse = self.browse(cr, uid, new_id, context=context)
2668         wh_stock_loc = warehouse.lot_stock_id
2669         wh_input_stock_loc = warehouse.wh_input_stock_loc_id
2670         wh_output_stock_loc = warehouse.wh_output_stock_loc_id
2671         wh_pack_stock_loc = warehouse.wh_pack_stock_loc_id
2672
2673         #fetch customer and supplier locations, for references
2674         customer_loc, supplier_loc = self._get_partner_locations(cr, uid, new_id, context=context)
2675
2676         #create in, out, internal picking types for warehouse
2677         input_loc = wh_input_stock_loc
2678         if warehouse.reception_steps == 'one_step':
2679             input_loc = wh_stock_loc
2680         output_loc = wh_output_stock_loc
2681         if warehouse.delivery_steps == 'ship_only':
2682             output_loc = wh_stock_loc
2683
2684         #choose the next available color for the picking types of this warehouse
2685         all_used_colors = self.pool.get('stock.picking.type').search_read(cr, uid, [('warehouse_id', '!=', False), ('color', '!=', False)], ['color'], order='color')
2686         not_used_colors = list(set(range(0, 9)) - set([x['color'] for x in all_used_colors]))
2687         color = 0
2688         if not_used_colors:
2689             color = not_used_colors[0]
2690
2691         in_type_id = picking_type_obj.create(cr, uid, vals={
2692             'name': _('Receptions'),
2693             'warehouse_id': new_id,
2694             'code_id': 'incoming',
2695             'auto_force_assign': True,
2696             'sequence_id': in_seq_id,
2697             'default_location_src_id': supplier_loc.id,
2698             'default_location_dest_id': input_loc.id,
2699             'color': color}, context=context)
2700         out_type_id = picking_type_obj.create(cr, uid, vals={
2701             'name': _('Delivery Orders'),
2702             'warehouse_id': new_id,
2703             'code_id': 'outgoing',
2704             'sequence_id': out_seq_id,
2705             'delivery': True,
2706             'default_location_src_id': output_loc.id,
2707             'default_location_dest_id': customer_loc.id,
2708             'color': color}, context=context)
2709         int_type_id = picking_type_obj.create(cr, uid, vals={
2710             'name': _('Internal Transfers'),
2711             'warehouse_id': new_id,
2712             'code_id': 'internal',
2713             'sequence_id': int_seq_id,
2714             'default_location_src_id': wh_stock_loc.id,
2715             'default_location_dest_id': wh_stock_loc.id,
2716             'active': reception_steps != 'one_step',
2717             'pack': False,
2718             'color': color}, context=context)
2719         pack_type_id = picking_type_obj.create(cr, uid, vals={
2720             'name': _('Pack'),
2721             'warehouse_id': new_id,
2722             'code_id': 'internal',
2723             'sequence_id': pack_seq_id,
2724             'default_location_src_id': wh_pack_stock_loc.id,
2725             'default_location_dest_id': output_loc.id,
2726             'active': delivery_steps == 'pick_pack_ship',
2727             'pack': True,
2728             'color': color}, context=context)
2729         pick_type_id = picking_type_obj.create(cr, uid, vals={
2730             'name': _('Pick'),
2731             'warehouse_id': new_id,
2732             'code_id': 'internal',
2733             'sequence_id': pick_seq_id,
2734             'default_location_src_id': wh_stock_loc.id,
2735             'default_location_dest_id': wh_pack_stock_loc.id,
2736             'active': delivery_steps != 'ship_only',
2737             'pack': False,
2738             'color': color}, context=context)
2739
2740         #write picking types on WH
2741         vals = {
2742             'in_type_id': in_type_id,
2743             'out_type_id': out_type_id,
2744             'pack_type_id': pack_type_id,
2745             'pick_type_id': pick_type_id,
2746             'int_type_id': int_type_id,
2747         }
2748         super(stock_warehouse, self).write(cr, uid, new_id, vals=vals, context=context)
2749         warehouse.refresh()
2750
2751         #create routes and push/pull rules
2752         new_objects_dict = self.create_routes(cr, uid, new_id, warehouse, context=context)
2753         self.write(cr, uid, warehouse.id, new_objects_dict, context=context)
2754         return new_id
2755
2756     def _format_rulename(self, cr, uid, obj, from_loc, dest_loc, context=None):
2757         return obj.code + ': ' + from_loc.name + ' -> ' + dest_loc.name
2758
2759     def _format_routename(self, cr, uid, obj, name, context=None):
2760         return obj.name + ': ' + name
2761
2762     def get_routes_dict(self, cr, uid, ids, warehouse, context=None):
2763         #fetch customer and supplier locations, for references
2764         customer_loc, supplier_loc = self._get_partner_locations(cr, uid, ids, context=context)
2765
2766         return {
2767             'one_step': (_('Reception in 1 step'), []),
2768             'two_steps': (_('Reception in 2 steps'), [(warehouse.wh_input_stock_loc_id, warehouse.lot_stock_id, warehouse.int_type_id.id)]),
2769             'three_steps': (_('Reception 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)]),
2770             '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)]),
2771             'ship_only': (_('Ship Only'), [(warehouse.lot_stock_id, customer_loc, warehouse.out_type_id.id)]),
2772             '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)]),
2773             'pick_pack_ship': (_('Pick + Pack + Ship'), [(warehouse.lot_stock_id, warehouse.wh_pack_stock_loc_id, warehouse.int_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)]),
2774         }
2775
2776     def _handle_renaming(self, cr, uid, warehouse, name, context=None):
2777         location_obj = self.pool.get('stock.location')
2778         route_obj = self.pool.get('stock.location.route')
2779         pull_obj = self.pool.get('procurement.rule')
2780         push_obj = self.pool.get('stock.location.path')
2781         #rename location
2782         location_id = warehouse.lot_stock_id.location_id.id
2783         location_obj.write(cr, uid, location_id, {'name': name}, context=context)
2784         #rename route and push-pull rules
2785         for route in warehouse.route_ids:
2786             route_obj.write(cr, uid, route.id, {'name': route.name.replace(warehouse.name, name, 1)}, context=context)
2787             for pull in route.pull_ids:
2788                 pull_obj.write(cr, uid, pull.id, {'name': pull.name.replace(warehouse.name, name, 1)}, context=context)
2789             for push in route.push_ids:
2790                 push_obj.write(cr, uid, push.id, {'name': pull.name.replace(warehouse.name, name, 1)}, context=context)
2791         #change the mto pull rule name
2792         pull_obj.write(cr, uid, warehouse.mto_pull_id.id, {'name': warehouse.mto_pull_id.name.replace(warehouse.name, name, 1)}, context=context)
2793
2794     def write(self, cr, uid, ids, vals, context=None):
2795         if context is None:
2796             context = {}
2797         if isinstance(ids, (int, long)):
2798             ids = [ids]
2799         seq_obj = self.pool.get('ir.sequence')
2800         route_obj = self.pool.get('stock.location.route')
2801         warehouse_obj = self.pool.get('stock.warehouse')
2802
2803         context_with_inactive = context.copy()
2804         context_with_inactive['active_test'] = False
2805         for warehouse in self.browse(cr, uid, ids, context=context_with_inactive):
2806             #first of all, check if we need to delete and recreate route
2807             if vals.get('reception_steps') or vals.get('delivery_steps'):
2808                 #activate and deactivate location according to reception and delivery option
2809                 self.switch_location(cr, uid, warehouse.id, warehouse, vals.get('reception_steps', False), vals.get('delivery_steps', False), context=context)
2810                 # switch between route
2811                 self.change_route(cr, uid, ids, warehouse, vals.get('reception_steps', False), vals.get('delivery_steps', False), context=context_with_inactive)
2812             if vals.get('code') or vals.get('name'):
2813                 name = warehouse.name
2814                 #rename sequence
2815                 if vals.get('name'):
2816                     name = vals.get('name')
2817                     self._handle_renaming(cr, uid, warehouse, name, context=context_with_inactive)
2818                 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)
2819                 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)
2820                 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)
2821                 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)
2822                 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)
2823         if vals.get('resupply_wh_ids') and not vals.get('resupply_route_ids'):
2824             for cmd in vals.get('resupply_wh_ids'):
2825                 if cmd[0] == 6:
2826                     new_ids = set(cmd[2])
2827                     old_ids = set([wh.id for wh in warehouse.resupply_wh_ids])
2828                     to_add_wh_ids = new_ids - old_ids
2829                     if to_add_wh_ids:
2830                         supplier_warehouses = warehouse_obj.browse(cr, uid, list(to_add_wh_ids), context=context)
2831                         self._create_resupply_routes(cr, uid, warehouse, supplier_warehouses, warehouse.default_resupply_wh_id, context=context)
2832                     to_remove_wh_ids = old_ids - new_ids
2833                     if to_remove_wh_ids:
2834                         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)
2835                         if to_remove_route_ids:
2836                             route_obj.unlink(cr, uid, to_remove_route_ids, context=context)
2837                 else:
2838                     #not implemented
2839                     pass
2840         if 'default_resupply_wh_id' in vals:
2841             if warehouse.default_resupply_wh_id:
2842                 #remove the existing resupplying route on all products
2843                 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)
2844                 for inter_wh_route_id in to_remove_route_ids:
2845                     self._unassign_route_on_products(cr, uid, warehouse, inter_wh_route_id, context=context)
2846             if vals.get('default_resupply_wh_id'):
2847                 #assign the new resupplying route on all products
2848                 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)
2849                 for inter_wh_route_id in to_assign_route_ids:
2850                     self._assign_route_on_products(cr, uid, warehouse, inter_wh_route_id, context=context)
2851
2852         return super(stock_warehouse, self).write(cr, uid, ids, vals=vals, context=context)
2853
2854     def unlink(self, cr, uid, ids, context=None):
2855         #TODO try to delete location and route and if not possible, put them in inactive
2856         return super(stock_warehouse, self).unlink(cr, uid, ids, context=context)
2857
2858     def get_all_routes_for_wh(self, cr, uid, warehouse, context=None):
2859         all_routes = [route.id for route in warehouse.route_ids]
2860         all_routes += [warehouse.mto_pull_id.route_id.id]
2861         return all_routes
2862
2863     def view_all_routes_for_wh(self, cr, uid, ids, context=None):
2864         all_routes = []
2865         for wh in self.browse(cr, uid, ids, context=context):
2866             all_routes += self.get_all_routes_for_wh(cr, uid, wh, context=context)
2867
2868         domain = [('id', 'in', all_routes)]
2869         return {
2870             'name': _('Warehouse\'s Routes'),
2871             'domain': domain,
2872             'res_model': 'stock.location.route',
2873             'type': 'ir.actions.act_window',
2874             'view_id': False,
2875             'view_mode': 'tree,form',
2876             'view_type': 'form',
2877             'limit': 20
2878         }
2879
2880 class stock_location_path(osv.osv):
2881     _name = "stock.location.path"
2882     _description = "Pushed Flows"
2883     _order = "name"
2884
2885     def _get_route(self, cr, uid, ids, context=None):
2886         #WARNING TODO route_id is not required, so a field related seems a bad idea >-< 
2887         if context is None:
2888             context = {}
2889         result = {}
2890         if context is None:
2891             context = {}
2892         context_with_inactive = context.copy()
2893         context_with_inactive['active_test'] = False
2894         for route in self.pool.get('stock.location.route').browse(cr, uid, ids, context=context_with_inactive):
2895             for push_rule in route.push_ids:
2896                 result[push_rule.id] = True
2897         return result.keys()
2898
2899     _columns = {
2900         'name': fields.char('Operation Name', size=64, required=True),
2901         'company_id': fields.many2one('res.company', 'Company'),
2902         'route_id': fields.many2one('stock.location.route', 'Route'),
2903         'location_from_id': fields.many2one('stock.location', 'Source Location', ondelete='cascade', select=1, required=True),
2904         'location_dest_id': fields.many2one('stock.location', 'Destination Location', ondelete='cascade', select=1, required=True),
2905         'delay': fields.integer('Delay (days)', help="Number of days to do this transition"),
2906         'invoice_state': fields.selection([
2907             ("invoiced", "Invoiced"),
2908             ("2binvoiced", "To Be Invoiced"),
2909             ("none", "Not Applicable")], "Invoice Status",
2910             required=True,), 
2911         '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"), 
2912         'auto': fields.selection(
2913             [('auto','Automatic Move'), ('manual','Manual Operation'),('transparent','Automatic No Step Added')],
2914             'Automatic Move',
2915             required=True, select=1,
2916             help="This is used to define paths the product has to follow within the location tree.\n" \
2917                 "The 'Automatic Move' value will create a stock move after the current one that will be "\
2918                 "validated automatically. With 'Manual Operation', the stock move has to be validated "\
2919                 "by a worker. With 'Automatic No Step Added', the location is replaced in the original move."
2920             ),
2921         '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'),
2922         'active': fields.related('route_id', 'active', type='boolean', string='Active', store={
2923                     'stock.location.route': (_get_route, ['active'], 20),
2924                     'stock.location.path': (lambda self, cr, uid, ids, c={}: ids, ['route_id'], 20),},
2925                 help="If the active field is set to False, it will allow you to hide the rule without removing it." ),
2926         'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse'),
2927     }
2928     _defaults = {
2929         'auto': 'auto',
2930         'delay': 1,
2931         'invoice_state': 'none',
2932         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'procurement.order', context=c),
2933         'propagate': True,
2934         'active': True,
2935     }
2936     def _apply(self, cr, uid, rule, move, context=None):
2937         move_obj = self.pool.get('stock.move')
2938         newdate = (datetime.strptime(move.date, '%Y-%m-%d %H:%M:%S') + relativedelta.relativedelta(days=rule.delay or 0)).strftime('%Y-%m-%d')
2939         if rule.auto=='transparent':
2940             move_obj.write(cr, uid, [move.id], {
2941                 'date': newdate,
2942                 'location_dest_id': rule.location_dest_id.id
2943             })
2944             if rule.location_dest_id.id<>move.location_dest_id.id:
2945                 move_obj._push_apply(self, cr, uid, move.id, context)
2946             return move.id
2947         else:
2948             move_id = move_obj.copy(cr, uid, move.id, {
2949                 'location_id': move.location_dest_id.id,
2950                 'location_dest_id': rule.location_dest_id.id,
2951                 'date': datetime.now().strftime('%Y-%m-%d'),
2952                 'company_id': rule.company_id and rule.company_id.id or False,
2953                 'date_expected': newdate,
2954                 'picking_id': False,
2955                 'picking_type_id': rule.picking_type_id and rule.picking_type_id.id or False,
2956                 'rule_id': rule.id,
2957                 'propagate': rule.propagate, 
2958                 'warehouse_id': rule.warehouse_id and rule.warehouse_id.id or False,
2959             })
2960             move_obj.write(cr, uid, [move.id], {
2961                 'move_dest_id': move_id,
2962             })
2963             move_obj.action_confirm(cr, uid, [move_id], context=None)
2964             return move_id
2965
2966 class stock_move_putaway(osv.osv):
2967     _name = 'stock.move.putaway'
2968     _description = 'Proposed Destination'
2969     _columns = {
2970         'move_id': fields.many2one('stock.move', required=True),
2971         'location_id': fields.many2one('stock.location', 'Location', required=True),
2972         'lot_id': fields.many2one('stock.production.lot', 'Lot'),
2973         'quantity': fields.float('Quantity', required=True),
2974     }
2975
2976
2977
2978 # -------------------------
2979 # Packaging related stuff
2980 # -------------------------
2981
2982 from openerp.report import report_sxw
2983 report_sxw.report_sxw('report.stock.quant.package.barcode', 'stock.quant.package', 'addons/stock/report/picking_barcode.rml')
2984
2985 class stock_package(osv.osv):
2986     """
2987     These are the packages, containing quants and/or other packages
2988     """
2989     _name = "stock.quant.package"
2990     _description = "Physical Packages"
2991     _parent_name = "parent_id"
2992     _parent_store = True
2993     _parent_order = 'name'
2994     _order = 'parent_left'
2995
2996     def name_get(self, cr, uid, ids, context=None):
2997         res = self._complete_name(cr, uid, ids, 'complete_name', None, context=context)
2998         return res.items()
2999
3000     def _complete_name(self, cr, uid, ids, name, args, context=None):
3001         """ Forms complete name of location from parent location to child location.
3002         @return: Dictionary of values
3003         """
3004         res = {}
3005         for m in self.browse(cr, uid, ids, context=context):
3006             res[m.id] = m.name
3007             parent = m.parent_id
3008             while parent:
3009                 res[m.id] = parent.name + ' / ' + res[m.id]
3010                 parent = parent.parent_id
3011         return res
3012
3013     def _get_packages(self, cr, uid, ids, context=None):
3014         """Returns packages from quants for store"""
3015         res = set()
3016         for quant in self.browse(cr, uid, ids, context=context):
3017             if quant.package_id:
3018                 res.add(quant.package_id.id)
3019         return list(res)
3020
3021     def _get_packages_to_relocate(self, cr, uid, ids, context=None):
3022         res = set()
3023         for pack in self.browse(cr, uid, ids, context=context):
3024             res.add(pack.id)
3025             if pack.parent_id:
3026                 res.add(pack.parent_id.id)
3027         return list(res)
3028
3029     # TODO: Problem when package is empty!
3030     #
3031     def _get_package_info(self, cr, uid, ids, name, args, context=None):
3032         default_company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
3033         res = {}.fromkeys(ids, {'location_id': False, 'company_id': default_company_id})
3034         for pack in self.browse(cr, uid, ids, context=context):
3035             if pack.quant_ids:
3036                 res[pack.id]['location_id'] = pack.quant_ids[0].location_id.id
3037                 res[pack.id]['owner_id'] = pack.quant_ids[0].owner_id and pack.quant_ids[0].owner_id.id or False
3038                 res[pack.id]['company_id'] = pack.quant_ids[0].company_id.id
3039             elif pack.children_ids:
3040                 res[pack.id]['location_id'] = pack.children_ids[0].location_id and pack.children_ids[0].location_id.id or False
3041                 res[pack.id]['owner_id'] = pack.children_ids[0].owner_id and pack.children_ids[0].owner_id.id or False
3042                 res[pack.id]['company_id'] = pack.children_ids[0].company_id and pack.children_ids[0].company_id.id or False
3043         return res
3044
3045     _columns = {
3046         'name': fields.char('Package Reference', size=64, select=True),
3047         'complete_name': fields.function(_complete_name, type='char', string="Package Name",),
3048         'parent_left': fields.integer('Left Parent', select=1),
3049         'parent_right': fields.integer('Right Parent', select=1),
3050         'packaging_id': fields.many2one('product.packaging', 'Type of Packaging'),
3051         'location_id': fields.function(_get_package_info, type='many2one', relation='stock.location', string='Location', multi="package",
3052                                     store={
3053                                        'stock.quant': (_get_packages, ['location_id'], 10),
3054                                        'stock.quant.package': (_get_packages_to_relocate, ['quant_ids', 'children_ids', 'parent_id'], 10),
3055                                     }, readonly=True),
3056         'quant_ids': fields.one2many('stock.quant', 'package_id', 'Bulk Content'),
3057         'parent_id': fields.many2one('stock.quant.package', 'Parent Package', help="The package containing this item", ondelete='restrict'),
3058         'children_ids': fields.one2many('stock.quant.package', 'parent_id', 'Contained Packages'),
3059         'company_id': fields.function(_get_package_info, type="many2one", relation='res.company', string='Company', multi="package", 
3060                                     store={
3061                                        'stock.quant': (_get_packages, ['company_id'], 10),
3062                                        'stock.quant.package': (_get_packages_to_relocate, ['quant_ids', 'children_ids', 'parent_id'], 10),
3063                                     }, readonly=True),
3064         'owner_id': fields.function(_get_package_info, type='many2one', relation='res.partner', string='Owner', multi="package",
3065                                 store={
3066                                        'stock.quant': (_get_packages, ['owner_id'], 10),
3067                                        'stock.quant.package': (_get_packages_to_relocate, ['quant_ids', 'children_ids', 'parent_id'], 10),
3068                                     }, readonly=True),
3069     }
3070     _defaults = {
3071         'name': lambda self, cr, uid, context: self.pool.get('ir.sequence').get(cr, uid, 'stock.quant.package') or _('Unknown Pack')
3072     }
3073     def _check_location(self, cr, uid, ids, context=None):
3074         '''checks that all quants in a package are stored in the same location'''
3075         quant_obj = self.pool.get('stock.quant')
3076         for pack in self.browse(cr, uid, ids, context=context):
3077             parent = pack
3078             while parent.parent_id:
3079                 parent = parent.parent_id
3080             quant_ids = self.get_content(cr, uid, [parent.id], context=context)
3081             quants = quant_obj.browse(cr, uid, quant_ids, context=context)
3082             location_id = quants and quants[0].location_id.id or False
3083             if not all([quant.location_id.id == location_id for quant in quants]):
3084                 return False
3085         return True
3086
3087     _constraints = [
3088         (_check_location, 'Everything inside a package should be in the same location', ['location_id']),
3089     ]
3090
3091     def action_print(self, cr, uid, ids, context=None):
3092         if context is None:
3093             context = {}
3094         datas = {
3095             'ids': context.get('active_id') and [context.get('active_id')] or ids,
3096             'model': 'stock.quant.package',
3097             'form': self.read(cr, uid, ids)[0]
3098         }
3099         return {
3100             'type': 'ir.actions.report.xml',
3101             'report_name': 'stock.quant.package.barcode',
3102             'datas': datas
3103         }
3104
3105     def unpack(self, cr, uid, ids, context=None):
3106         quant_obj = self.pool.get('stock.quant')
3107         for package in self.browse(cr, uid, ids, context=context):
3108             quant_ids = [quant.id for quant in package.quant_ids]
3109             quant_obj.write(cr, uid, quant_ids, {'package_id': package.parent_id.id or False}, context=context)
3110             children_package_ids = [child_package.id for child_package in package.children_ids]
3111             self.write(cr, uid, children_package_ids, {'parent_id': package.parent_id.id or False}, context=context)
3112         #delete current package since it contains nothing anymore
3113         self.unlink(cr, uid, ids, context=context)
3114         return self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'stock', 'action_package_view', context=context)
3115
3116     def get_content(self, cr, uid, ids, context=None):
3117         child_package_ids = self.search(cr, uid, [('id', 'child_of', ids)], context=context)
3118         return self.pool.get('stock.quant').search(cr, uid, [('package_id', 'in', child_package_ids)], context=context)
3119
3120     def get_content_package(self, cr, uid, ids, context=None):
3121         quants_ids = self.get_content(cr, uid, ids, context=context)
3122         res = self.pool.get('ir.actions.act_window').for_xml_id(cr, uid, 'stock', 'quantsact', context=context)
3123         res['domain'] = [('id', 'in', quants_ids)]
3124         return res
3125
3126     def _get_product_total_qty(self, cr, uid, package_record, product_id, context=None):
3127         ''' find the total of given product 'product_id' inside the given package 'package_id'''
3128         quant_obj = self.pool.get('stock.quant')
3129         all_quant_ids = self.get_content(cr, uid, [package_record.id], context=context)
3130         total = 0
3131         for quant in quant_obj.browse(cr, uid, all_quant_ids, context=context):
3132             if quant.product_id.id == product_id:
3133                 total += quant.qty
3134         return total
3135
3136
3137 class stock_pack_operation(osv.osv):
3138     _name = "stock.pack.operation"
3139     _description = "Packing Operation"
3140
3141     def _get_remaining_qty(self, cr, uid, ids, name, args, context=None):
3142         res = {}
3143         for ops in self.browse(cr, uid, ids, context=context):
3144             qty = ops.product_qty
3145             for quant in ops.reserved_quant_ids:
3146                 qty -= quant.qty
3147             res[ops.id] = qty
3148         return res
3149
3150     def product_id_change(self, cr, uid, ids, product_id, product_uom_id, product_qty, context=None):
3151         res = self.on_change_tests(cr, uid, ids, product_id, product_uom_id, product_qty, context=context)
3152         if product_id and not product_uom_id:
3153             product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
3154             res['value']['product_uom_id'] = product.uom_id.id
3155         return res
3156
3157     def on_change_tests(self, cr, uid, ids, product_id, product_uom_id, product_qty, context=None):
3158         res = {'value': {}}
3159         uom_obj = self.pool.get('product.uom')
3160         if product_id:
3161             product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
3162             product_uom_id = product_uom_id or product.uom_id.id
3163             selected_uom = uom_obj.browse(cr, uid, product_uom_id, context=context)
3164             if selected_uom.category_id.id != product.uom_id.category_id.id:
3165                 res['warning'] = {
3166                     'title': _('Warning: wrong UoM!'),
3167                     '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)
3168                 }
3169             if product_qty and 'warning' not in res:
3170                 rounded_qty = uom_obj._compute_qty(cr, uid, product_uom_id, product_qty, product_uom_id, round=True)
3171                 if rounded_qty != product_qty:
3172                     res['warning'] = {
3173                         'title': _('Warning: wrong quantity!'),
3174                         'message': _('The chosen quantity for product %s is not compatible with the UoM rounding. It will be automatically converted at confirmation') % (product.name)
3175                     }
3176         return res
3177
3178     _columns = {
3179         'picking_id': fields.many2one('stock.picking', 'Stock Picking', help='The stock operation where the packing has been made', required=True),
3180         'product_id': fields.many2one('product.product', 'Product', ondelete="CASCADE"),  # 1
3181         'product_uom_id': fields.many2one('product.uom', 'Product Unit of Measure'),
3182         'product_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
3183         'package_id': fields.many2one('stock.quant.package', 'Package'),  # 2
3184         'quant_id': fields.many2one('stock.quant', 'Quant'),  # 3
3185         'lot_id': fields.many2one('stock.production.lot', 'Lot/Serial Number'),
3186         'result_package_id': fields.many2one('stock.quant.package', 'Container Package', help="If set, the operations are packed into this package", required=False, ondelete='cascade'),
3187         'date': fields.datetime('Date', required=True),
3188         'owner_id': fields.many2one('res.partner', 'Owner', help="Owner of the quants"),
3189         #'update_cost': fields.boolean('Need cost update'),
3190         'cost': fields.float("Cost", help="Unit Cost for this product line"),
3191         'currency': fields.many2one('res.currency', string="Currency", help="Currency in which Unit cost is expressed", ondelete='CASCADE'),
3192         'reserved_quant_ids': fields.one2many('stock.quant', 'reservation_op_id', string='Reserved Quants', readonly=True, help='Quants reserved for this operation'),
3193         'remaining_qty': fields.function(_get_remaining_qty, type='float', string='Remaining Qty'),
3194     }
3195
3196     _defaults = {
3197         'date': fields.date.context_today,
3198     }
3199
3200     def _get_domain(self, cr, uid, ops, context=None):
3201         '''
3202             Gives domain for different
3203         '''
3204         res = []
3205         if ops.package_id:
3206             res.append(('package_id', '=', ops.package_id.id), )
3207         if ops.lot_id:
3208             res.append(('lot_id', '=', ops.lot_id.id), )
3209         if ops.owner_id: 
3210             res.append(('owner_id', '=', ops.owner_id.id), )
3211         else:
3212             res.append(('owner_id', '=', False), )
3213         return res
3214
3215     #TODO: this function can be refactored
3216     def _search_and_increment(self, cr, uid, picking_id, key, context=None):
3217         '''Search for an operation on an existing key in a picking, if it exists increment the qty (+1) otherwise create it
3218
3219         :param key: tuple directly reusable in a domain
3220         context can receive a key 'current_package_id' with the package to consider for this operation
3221         returns True
3222
3223         previously: returns the update to do in stock.move one2many field of picking (adapt remaining quantities) and to the list of package in the classic one2many syntax
3224                  (0, 0,  { values })    link to a new record that needs to be created with the given values dictionary
3225                  (1, ID, { values })    update the linked record with id = ID (write *values* on it)
3226                  (2, ID)                remove and delete the linked record with id = ID (calls unlink on ID, that will delete the object completely, and the link to it as well)
3227         '''
3228         quant_obj = self.pool.get('stock.quant')
3229         if context is None:
3230             context = {}
3231
3232         #if current_package_id is given in the context, we increase the number of items in this package
3233         package_clause = [('result_package_id', '=', context.get('current_package_id', False))]
3234         existing_operation_ids = self.search(cr, uid, [('picking_id', '=', picking_id), key] + package_clause, context=context)
3235         if existing_operation_ids:
3236             #existing operation found for the given key and picking => increment its quantity
3237             operation_id = existing_operation_ids[0]
3238             qty = self.browse(cr, uid, operation_id, context=context).product_qty + 1
3239             self.write(cr, uid, operation_id, {'product_qty': qty}, context=context)
3240         else:
3241             #no existing operation found for the given key and picking => create a new one
3242             var_name, dummy, value = key
3243             uom_id = False
3244             if var_name == 'product_id':
3245                 uom_id = self.pool.get('product.product').browse(cr, uid, value, context=context).uom_id.id
3246             elif var_name == 'quant_id':
3247                 quant = quant_obj.browse(cr, uid, value, context=context)
3248                 uom_id = quant.product_id.uom_id.id
3249             values = {
3250                 'picking_id': picking_id,
3251                 var_name: value,
3252                 'product_qty': 1,
3253                 'product_uom_id': uom_id,
3254             }
3255             operation_id = self.create(cr, uid, values, context=context)
3256             values.update({'id': operation_id})
3257         return True
3258
3259 class stock_warehouse_orderpoint(osv.osv):
3260     """
3261     Defines Minimum stock rules.
3262     """
3263     _name = "stock.warehouse.orderpoint"
3264     _description = "Minimum Inventory Rule"
3265
3266     def get_draft_procurements(self, cr, uid, ids, context=None):
3267         if context is None:
3268             context = {}
3269         result = {}
3270         if not isinstance(ids, list):
3271             ids = [ids]
3272         procurement_obj = self.pool.get('procurement.order')
3273         for orderpoint in self.browse(cr, uid, ids, context=context):
3274             procurement_ids = procurement_obj.search(cr, uid, [('state', 'not in', ('cancel', 'done')), ('product_id', '=', orderpoint.product_id.id), ('location_id', '=', orderpoint.location_id.id)], context=context)            
3275         return list(set(procurement_ids))
3276
3277     def _check_product_uom(self, cr, uid, ids, context=None):
3278         '''
3279         Check if the UoM has the same category as the product standard UoM
3280         '''
3281         if not context:
3282             context = {}
3283
3284         for rule in self.browse(cr, uid, ids, context=context):
3285             if rule.product_id.uom_id.category_id.id != rule.product_uom.category_id.id:
3286                 return False
3287
3288         return True
3289
3290     def action_view_proc_to_process(self, cr, uid, ids, context=None):        
3291         act_obj = self.pool.get('ir.actions.act_window')
3292         mod_obj = self.pool.get('ir.model.data')
3293         draft_ids = self.get_draft_procurements(cr, uid, ids, context=context)
3294         result = mod_obj.get_object_reference(cr, uid, 'procurement', 'do_view_procurements')
3295         if not result:
3296             return False
3297  
3298         result = act_obj.read(cr, uid, [result[1]], context=context)[0]
3299         result['domain'] = "[('id', 'in', [" + ','.join(map(str, draft_ids)) + "])]"
3300         return result
3301
3302     _columns = {
3303         'name': fields.char('Name', size=32, required=True),
3304         'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the orderpoint without removing it."),
3305         'logic': fields.selection([('max', 'Order to Max'), ('price', 'Best price (not yet active!)')], 'Reordering Mode', required=True),
3306         'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', required=True, ondelete="cascade"),
3307         'location_id': fields.many2one('stock.location', 'Location', required=True, ondelete="cascade"),
3308         'product_id': fields.many2one('product.product', 'Product', required=True, ondelete='cascade', domain=[('type', '!=', 'service')]),
3309         'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
3310         'product_min_qty': fields.float('Minimum Quantity', required=True,
3311             help="When the virtual stock goes below the Min Quantity specified for this field, OpenERP generates "\
3312             "a procurement to bring the forecasted quantity to the Max Quantity."),
3313         'product_max_qty': fields.float('Maximum Quantity', required=True,
3314             help="When the virtual stock goes below the Min Quantity, OpenERP generates "\
3315             "a procurement to bring the forecasted quantity to the Quantity specified as Max Quantity."),
3316         'qty_multiple': fields.integer('Qty Multiple', required=True,
3317             help="The procurement quantity will be rounded up to this multiple."),
3318         'procurement_id': fields.many2one('procurement.order', 'Latest procurement', ondelete="set null"),
3319         'company_id': fields.many2one('res.company', 'Company', required=True)        
3320     }
3321     _defaults = {
3322         'active': lambda *a: 1,
3323         'logic': lambda *a: 'max',
3324         'qty_multiple': lambda *a: 1,
3325         'name': lambda self, cr, uid, context: self.pool.get('ir.sequence').get(cr, uid, 'stock.orderpoint') or '',
3326         'product_uom': lambda self, cr, uid, context: context.get('product_uom', False),
3327         'company_id': lambda self, cr, uid, context: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.warehouse.orderpoint', context=context)
3328     }
3329     _sql_constraints = [
3330         ('qty_multiple_check', 'CHECK( qty_multiple > 0 )', 'Qty Multiple must be greater than zero.'),
3331     ]
3332     _constraints = [
3333         (_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']),
3334     ]
3335
3336     def default_get(self, cr, uid, fields, context=None):
3337         res = super(stock_warehouse_orderpoint, self).default_get(cr, uid, fields, context)
3338         # default 'warehouse_id' and 'location_id'
3339         if 'warehouse_id' not in res:
3340             warehouse = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'warehouse0', context)
3341             res['warehouse_id'] = warehouse.id
3342         if 'location_id' not in res:
3343             warehouse = self.pool.get('stock.warehouse').browse(cr, uid, res['warehouse_id'], context)
3344             res['location_id'] = warehouse.lot_stock_id.id
3345         return res
3346
3347     def onchange_warehouse_id(self, cr, uid, ids, warehouse_id, context=None):
3348         """ Finds location id for changed warehouse.
3349         @param warehouse_id: Changed id of warehouse.
3350         @return: Dictionary of values.
3351         """
3352         if warehouse_id:
3353             w = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context)
3354             v = {'location_id': w.lot_stock_id.id}
3355             return {'value': v}
3356         return {}
3357
3358     def onchange_product_id(self, cr, uid, ids, product_id, context=None):
3359         """ Finds UoM for changed product.
3360         @param product_id: Changed id of product.
3361         @return: Dictionary of values.
3362         """
3363         if product_id:
3364             prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
3365             d = {'product_uom': [('category_id', '=', prod.uom_id.category_id.id)]}
3366             v = {'product_uom': prod.uom_id.id}
3367             return {'value': v, 'domain': d}
3368         return {'domain': {'product_uom': []}}
3369
3370     def copy(self, cr, uid, id, default=None, context=None):
3371         if not default:
3372             default = {}
3373         default.update({
3374             'name': self.pool.get('ir.sequence').get(cr, uid, 'stock.orderpoint') or '',
3375         })
3376         return super(stock_warehouse_orderpoint, self).copy(cr, uid, id, default, context=context)
3377
3378
3379
3380 class stock_picking_type(osv.osv):
3381     _name = "stock.picking.type"
3382     _description = "The picking type determines the picking view"
3383
3384     def __get_bar_values(self, cr, uid, obj, domain, read_fields, value_field, groupby_field, context=None):
3385         """ Generic method to generate data for bar chart values using SparklineBarWidget.
3386             This method performs obj.read_group(cr, uid, domain, read_fields, groupby_field).
3387
3388             :param obj: the target model (i.e. crm_lead)
3389             :param domain: the domain applied to the read_group
3390             :param list read_fields: the list of fields to read in the read_group
3391             :param str value_field: the field used to compute the value of the bar slice
3392             :param str groupby_field: the fields used to group
3393
3394             :return list section_result: a list of dicts: [
3395                                                 {   'value': (int) bar_column_value,
3396                                                     'tootip': (str) bar_column_tooltip,
3397                                                 }
3398                                             ]
3399         """
3400         month_begin = date.today().replace(day=1)
3401         section_result = [{
3402                             'value': 0,
3403                             'tooltip': (month_begin + relativedelta.relativedelta(months=-i)).strftime('%B'),
3404                             } for i in range(10, -1, -1)]
3405         group_obj = obj.read_group(cr, uid, domain, read_fields, groupby_field, context=context)
3406         for group in group_obj:
3407             group_begin_date = datetime.strptime(group['__domain'][0][2], tools.DEFAULT_SERVER_DATE_FORMAT)
3408             month_delta = relativedelta.relativedelta(month_begin, group_begin_date)
3409             section_result[10 - (month_delta.months + 1)] = {'value': group.get(value_field, 0), 'tooltip': group_begin_date.strftime('%B')}
3410         return section_result
3411
3412     def _get_picking_data(self, cr, uid, ids, field_name, arg, context=None):
3413         obj = self.pool.get('stock.picking')
3414         res = dict.fromkeys(ids, False)
3415         month_begin = date.today().replace(day=1)
3416         groupby_begin = (month_begin + relativedelta.relativedelta(months=-4)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
3417         groupby_end = (month_begin + relativedelta.relativedelta(months=3)).strftime(tools.DEFAULT_SERVER_DATE_FORMAT)
3418         for id in ids:
3419             created_domain = [
3420                 ('picking_type_id', '=', id),
3421                 ('state', 'not in', ['done', 'cancel']),
3422                 ('date', '>=', groupby_begin),
3423                 ('date', '<', groupby_end),
3424             ]
3425             res[id] = self.__get_bar_values(cr, uid, obj, created_domain, ['date'], 'picking_type_id_count', 'date', context=context)
3426         return res
3427
3428     def _get_picking_count(self, cr, uid, ids, field_names, arg, context=None):
3429         obj = self.pool.get('stock.picking')
3430         domains = {
3431             'count_picking_draft': [('state', '=', 'draft')],
3432             'count_picking_waiting': [('state','=','confirmed')],
3433             'count_picking_ready': [('state','=','assigned')],
3434             'count_picking': [('state','in',('assigned','waiting','confirmed'))],
3435             'count_picking_late': [('min_date','<', time.strftime('%Y-%m-%d %H:%M:%S')), ('state','in',('assigned','waiting','confirmed'))],
3436             'count_picking_backorders': [('backorder_id','<>', False), ('state','!=','done')],
3437         }
3438         result = {}
3439         for field in domains:
3440             data = obj.read_group(cr, uid, domains[field] +
3441                 [('state', 'not in',('done','cancel')), ('picking_type_id', 'in', ids)],
3442                 ['picking_type_id'], ['picking_type_id'], context=context)
3443             count = dict(map(lambda x: (x['picking_type_id'] and x['picking_type_id'][0], x['picking_type_id_count']), data))
3444             for tid in ids:
3445                 result.setdefault(tid, {})[field] = count.get(tid, 0)
3446         for tid in ids:
3447             if result[tid]['count_picking']:
3448                 result[tid]['rate_picking_late'] = result[tid]['count_picking_late'] *100 / result[tid]['count_picking']
3449                 result[tid]['rate_picking_backorders'] = result[tid]['count_picking_backorders'] *100 / (result[tid]['count_picking'] + result[tid]['count_picking_draft'])
3450             else:
3451                 result[tid]['rate_picking_late'] = 0
3452                 result[tid]['rate_picking_backorders'] = 0
3453         return result
3454
3455     #TODO: not returning valus in required format to show in sparkline library,just added latest_picking_waiting need to add proper logic.
3456     def _get_picking_history(self, cr, uid, ids, field_names, arg, context=None):
3457         obj = self.pool.get('stock.picking')
3458         result = {}
3459         for id in ids:
3460             result[id] = {
3461                 'latest_picking_late': [],
3462                 'latest_picking_backorders': [],
3463                 'latest_picking_waiting': []
3464             }
3465         for type_id in ids:
3466             pick_ids = obj.search(cr, uid, [('state', '=','done'), ('picking_type_id','=',type_id)], limit=12, order="date desc", context=context)
3467             for pick in obj.browse(cr, uid, pick_ids, context=context):
3468                 result[type_id]['latest_picking_late'] = cmp(pick.date[:10], time.strftime('%Y-%m-%d'))
3469                 result[type_id]['latest_picking_backorders'] = bool(pick.backorder_id)
3470                 result[type_id]['latest_picking_waiting'] = cmp(pick.date[:10], time.strftime('%Y-%m-%d'))
3471         return result
3472
3473     def onchange_picking_code(self, cr, uid, ids, picking_code=False):
3474         if not picking_code:
3475             return False
3476         
3477         obj_data = self.pool.get('ir.model.data')
3478         stock_loc = obj_data.get_object_reference(cr, uid, 'stock','stock_location_stock')[1]
3479         
3480         result = {
3481             'default_location_src_id': stock_loc,
3482             'default_location_dest_id': stock_loc,
3483         }
3484         if picking_code == 'incoming':
3485             result['default_location_src_id'] = obj_data.get_object_reference(cr, uid, 'stock','stock_location_suppliers')[1]
3486             return {'value': result}
3487         if picking_code == 'outgoing':
3488             result['default_location_dest_id'] = obj_data.get_object_reference(cr, uid, 'stock','stock_location_customers')[1]
3489             return {'value': result}
3490         else:
3491             return {'value': result}
3492
3493     def _get_name(self, cr, uid, ids, field_names, arg, context=None):
3494         return dict(self.name_get(cr, uid, ids, context=context))
3495
3496     def name_get(self, cr, uid, ids, context=None):
3497         """Overides orm name_get method to display 'Warehouse_name: PickingType_name' """
3498         if context is None:
3499             context = {}
3500         if not isinstance(ids, list):
3501             ids = [ids]
3502         res = []
3503         if not ids:
3504             return res
3505         for record in self.browse(cr, uid, ids, context=context):
3506             name = record.name
3507             if record.warehouse_id:
3508                 name = record.warehouse_id.name + ': ' +name
3509             if context.get('special_shortened_wh_name'):
3510                 if record.warehouse_id:
3511                     name = record.warehouse_id.name
3512                 else:
3513                     name = _('Customer') + ' (' + record.name + ')'
3514             res.append((record.id, name))
3515         return res
3516
3517     def _default_warehouse(self, cr, uid, context=None):
3518         user = self.pool.get('res.users').browse(cr, uid, uid, context)
3519         res = self.pool.get('stock.warehouse').search(cr, uid, [('company_id', '=', user.company_id.id)], limit=1, context=context)
3520         return res and res[0] or False
3521
3522     _columns = {
3523         'name': fields.char('Name', translate=True, required=True),
3524         'complete_name': fields.function(_get_name, type='char', string='Name'),
3525         'pack': fields.boolean('Prefill Pack Operations', help='This picking type needs packing interface'),
3526         'auto_force_assign': fields.boolean('Automatic Availability', help='This picking type does\'t need to check for the availability in source location.'),
3527         'color': fields.integer('Color'),
3528         'delivery': fields.boolean('Print delivery'),
3529         'sequence_id': fields.many2one('ir.sequence', 'Reference Sequence', required=True),
3530         'default_location_src_id': fields.many2one('stock.location', 'Default Source Location'),
3531         'default_location_dest_id': fields.many2one('stock.location', 'Default Destination Location'),
3532         #TODO: change field name to "code" as it's not a many2one anymore
3533         'code_id': fields.selection([('incoming', 'Suppliers'), ('outgoing', 'Customers'), ('internal', 'Internal')], 'Picking type code', required=True),
3534         'return_picking_type_id': fields.many2one('stock.picking.type', 'Picking Type for Returns'),
3535         'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', ondelete='cascade'),
3536         'active': fields.boolean('Active'),
3537
3538         # Statistics for the kanban view
3539         'weekly_picking': fields.function(_get_picking_data,
3540             type='string',
3541             string='Scheduled pickings per week'),
3542
3543         'count_picking_draft': fields.function(_get_picking_count,
3544             type='integer', multi='_get_picking_count'),
3545         'count_picking_ready': fields.function(_get_picking_count,
3546             type='integer', multi='_get_picking_count'),
3547         'count_picking': fields.function(_get_picking_count,
3548             type='integer', multi='_get_picking_count'),
3549         'count_picking_waiting': fields.function(_get_picking_count,
3550             type='integer', multi='_get_picking_count'),
3551         'count_picking_late': fields.function(_get_picking_count,
3552             type='integer', multi='_get_picking_count'),
3553         'count_picking_backorders': fields.function(_get_picking_count,
3554             type='integer', multi='_get_picking_count'),
3555
3556         'rate_picking_late': fields.function(_get_picking_count,
3557             type='integer', multi='_get_picking_count'),
3558         'rate_picking_backorders': fields.function(_get_picking_count,
3559             type='integer', multi='_get_picking_count'),
3560
3561         'latest_picking_late': fields.function(_get_picking_history,
3562             type='string', multi='_get_picking_history'),
3563         'latest_picking_backorders': fields.function(_get_picking_history,
3564             type='string', multi='_get_picking_history'),
3565         'latest_picking_waiting': fields.function(_get_picking_history,
3566             type='string', multi='_get_picking_history'),
3567
3568     }
3569     _defaults = {
3570         'warehouse_id': _default_warehouse,
3571         'active': True,
3572     }
3573
3574
3575 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: