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