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