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