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