Modularize the selection of the partner to invoice when invoicing from the picking.
[odoo/odoo.git] / addons / stock / stock.py
1 ##############################################################################
2 #
3 #    OpenERP, Open Source Management Solution
4 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
5 #
6 #    This program is free software: you can redistribute it and/or modify
7 #    it under the terms of the GNU Affero General Public License as
8 #    published by the Free Software Foundation, either version 3 of the
9 #    License, or (at your option) any later version.
10 #
11 #    This program is distributed in the hope that it will be useful,
12 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
13 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 #    GNU Affero General Public License for more details.
15 #
16 #    You should have received a copy of the GNU Affero General Public License
17 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
18 #
19 ##############################################################################
20
21 from datetime import datetime
22 from dateutil.relativedelta import relativedelta
23 import time
24 from operator import itemgetter
25 from itertools import groupby
26
27 from osv import fields, osv
28 from tools.translate import _
29 import netsvc
30 import tools
31 import decimal_precision as dp
32 import logging
33
34
35 #----------------------------------------------------------
36 # Incoterms
37 #----------------------------------------------------------
38 class stock_incoterms(osv.osv):
39     _name = "stock.incoterms"
40     _description = "Incoterms"
41     _columns = {
42         'name': fields.char('Name', size=64, required=True, help="Incoterms are series of sales terms.They are used to divide transaction costs and responsibilities between buyer and seller and reflect state-of-the-art transportation practices."),
43         'code': fields.char('Code', size=3, required=True, help="Code for Incoterms"),
44         'active': fields.boolean('Active', help="By unchecking the active field, you may hide an INCOTERM without deleting it."),
45     }
46     _defaults = {
47         'active': True,
48     }
49
50 stock_incoterms()
51
52 class stock_journal(osv.osv):
53     _name = "stock.journal"
54     _description = "Stock Journal"
55     _columns = {
56         'name': fields.char('Stock Journal', size=32, required=True),
57         'user_id': fields.many2one('res.users', 'Responsible'),
58     }
59     _defaults = {
60         'user_id': lambda s, c, u, ctx: u
61     }
62
63 stock_journal()
64
65 #----------------------------------------------------------
66 # Stock Location
67 #----------------------------------------------------------
68 class stock_location(osv.osv):
69     _name = "stock.location"
70     _description = "Location"
71     _parent_name = "location_id"
72     _parent_store = True
73     _parent_order = 'posz,name'
74     _order = 'parent_left'
75
76     def name_get(self, cr, uid, ids, context=None):
77         res = []
78         if context is None:
79             context = {}
80         if not len(ids):
81             return []
82         reads = self.read(cr, uid, ids, ['name','location_id'], context=context)
83         for record in reads:
84             name = record['name']
85             if context.get('full',False):
86                 if record['location_id']:
87                     name = record['location_id'][1] + ' / ' + name
88                 res.append((record['id'], name))
89             else:
90                 res.append((record['id'], name))
91         return res
92
93     def _complete_name(self, cr, uid, ids, name, args, context=None):
94         """ Forms complete name of location from parent location to child location.
95         @return: Dictionary of values
96         """
97         def _get_one_full_name(location, level=4):
98             if location.location_id:
99                 parent_path = _get_one_full_name(location.location_id, level-1) + "/"
100             else:
101                 parent_path = ''
102             return parent_path + location.name
103         res = {}
104         for m in self.browse(cr, uid, ids, context=context):
105             res[m.id] = _get_one_full_name(m)
106         return res
107
108
109     def _product_value(self, cr, uid, ids, field_names, arg, context=None):
110         """Computes stock value (real and virtual) for a product, as well as stock qty (real and virtual).
111         @param field_names: Name of field
112         @return: Dictionary of values
113         """
114         prod_id = context and context.get('product_id', False)
115
116         if not prod_id:
117             return dict([(i, {}.fromkeys(field_names, 0.0)) for i in ids])
118
119         product_product_obj = self.pool.get('product.product')
120
121         cr.execute('select distinct product_id, location_id from stock_move where location_id in %s', (tuple(ids), ))
122         dict1 = cr.dictfetchall()
123         cr.execute('select distinct product_id, location_dest_id as location_id from stock_move where location_dest_id in %s', (tuple(ids), ))
124         dict2 = cr.dictfetchall()
125         res_products_by_location = sorted(dict1+dict2, key=itemgetter('location_id'))
126         products_by_location = dict((k, [v['product_id'] for v in itr]) for k, itr in groupby(res_products_by_location, itemgetter('location_id')))
127
128         result = dict([(i, {}.fromkeys(field_names, 0.0)) for i in ids])
129         result.update(dict([(i, {}.fromkeys(field_names, 0.0)) for i in list(set([aaa['location_id'] for aaa in res_products_by_location]))]))
130
131         currency_id = self.pool.get('res.users').browse(cr, uid, uid).company_id.currency_id.id
132         currency_obj = self.pool.get('res.currency')
133         currency = currency_obj.browse(cr, uid, currency_id, context=context)
134         for loc_id, product_ids in products_by_location.items():
135             if prod_id:
136                 product_ids = [prod_id]
137             c = (context or {}).copy()
138             c['location'] = loc_id
139             for prod in product_product_obj.browse(cr, uid, product_ids, context=c):
140                 for f in field_names:
141                     if f == 'stock_real':
142                         if loc_id not in result:
143                             result[loc_id] = {}
144                         result[loc_id][f] += prod.qty_available
145                     elif f == 'stock_virtual':
146                         result[loc_id][f] += prod.virtual_available
147                     elif f == 'stock_real_value':
148                         amount = prod.qty_available * prod.standard_price
149                         amount = currency_obj.round(cr, uid, currency, amount)
150                         result[loc_id][f] += amount
151                     elif f == 'stock_virtual_value':
152                         amount = prod.virtual_available * prod.standard_price
153                         amount = currency_obj.round(cr, uid, currency, amount)
154                         result[loc_id][f] += amount
155         return result
156
157     _columns = {
158         'name': fields.char('Location Name', size=64, required=True, translate=True),
159         'active': fields.boolean('Active', help="By unchecking the active field, you may hide a location without deleting it."),
160         'usage': fields.selection([('supplier', 'Supplier Location'), ('view', 'View'), ('internal', 'Internal Location'), ('customer', 'Customer Location'), ('inventory', 'Inventory'), ('procurement', 'Procurement'), ('production', 'Production'), ('transit', 'Transit Location for Inter-Companies Transfers')], 'Location Type', required=True,
161                  help="""* Supplier Location: Virtual location representing the source location for products coming from your suppliers
162                        \n* View: Virtual location used to create a hierarchical structures for your warehouse, aggregating its child locations ; can't directly contain products
163                        \n* Internal Location: Physical locations inside your own warehouses,
164                        \n* Customer Location: Virtual location representing the destination location for products sent to your customers
165                        \n* Inventory: Virtual location serving as counterpart for inventory operations used to correct stock levels (Physical inventories)
166                        \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.
167                        \n* Production: Virtual counterpart location for production operations: this location consumes the raw material and produces finished products
168                       """, select = True),
169          # temporarily removed, as it's unused: 'allocation_method': fields.selection([('fifo', 'FIFO'), ('lifo', 'LIFO'), ('nearest', 'Nearest')], 'Allocation Method', required=True),
170         'complete_name': fields.function(_complete_name, type='char', size=256, string="Location Name", store=True),
171
172         'stock_real': fields.function(_product_value, type='float', string='Real Stock', multi="stock"),
173         'stock_virtual': fields.function(_product_value, type='float', string='Virtual Stock', multi="stock"),
174
175         'location_id': fields.many2one('stock.location', 'Parent Location', select=True, ondelete='cascade'),
176         'child_ids': fields.one2many('stock.location', 'location_id', 'Contains'),
177
178         'chained_journal_id': fields.many2one('stock.journal', 'Chaining Journal',help="Inventory Journal in which the chained move will be written, if the Chaining Type is not Transparent (no journal is used if left empty)"),
179         'chained_location_id': fields.many2one('stock.location', 'Chained Location If Fixed'),
180         'chained_location_type': fields.selection([('none', 'None'), ('customer', 'Customer'), ('fixed', 'Fixed Location')],
181             'Chained Location Type', required=True,
182             help="Determines whether this location is chained to another location, i.e. any incoming product in this location \n" \
183                 "should next go to the chained location. The chained location is determined according to the type :"\
184                 "\n* None: No chaining at all"\
185                 "\n* Customer: The chained location will be taken from the Customer Location field on the Partner form of the Partner that is specified in the Picking list of the incoming products." \
186                 "\n* Fixed Location: The chained location is taken from the next field: Chained Location if Fixed." \
187                 ),
188         'chained_auto_packing': fields.selection(
189             [('auto', 'Automatic Move'), ('manual', 'Manual Operation'), ('transparent', 'Automatic No Step Added')],
190             'Chaining Type',
191             required=True,
192             help="This is used only if you select a chained location type.\n" \
193                 "The 'Automatic Move' value will create a stock move after the current one that will be "\
194                 "validated automatically. With 'Manual Operation', the stock move has to be validated "\
195                 "by a worker. With 'Automatic No Step Added', the location is replaced in the original move."
196             ),
197         'chained_picking_type': fields.selection([('out', 'Sending Goods'), ('in', 'Getting Goods'), ('internal', 'Internal')], 'Shipping Type', help="Shipping Type of the Picking List that will contain the chained move (leave empty to automatically detect the type based on the source and destination locations)."),
198         'chained_company_id': fields.many2one('res.company', 'Chained Company', help='The company the Picking List containing the chained move will belong to (leave empty to use the default company determination rules'),
199         'chained_delay': fields.integer('Chaining Lead Time',help="Delay between original move and chained move in days"),
200         'address_id': fields.many2one('res.partner.address', 'Location Address',help="Address of  customer or supplier."),
201         'icon': fields.selection(tools.icons, 'Icon', size=64,help="Icon show in  hierarchical tree view"),
202
203         'comment': fields.text('Additional Information'),
204         'posx': fields.integer('Corridor (X)',help="Optional localization details, for information purpose only"),
205         'posy': fields.integer('Shelves (Y)', help="Optional localization details, for information purpose only"),
206         'posz': fields.integer('Height (Z)', help="Optional localization details, for information purpose only"),
207
208         'parent_left': fields.integer('Left Parent', select=1),
209         'parent_right': fields.integer('Right Parent', select=1),
210         'stock_real_value': fields.function(_product_value, type='float', string='Real Stock Value', multi="stock", digits_compute=dp.get_precision('Account')),
211         'stock_virtual_value': fields.function(_product_value, type='float', string='Virtual Stock Value', multi="stock", digits_compute=dp.get_precision('Account')),
212         'company_id': fields.many2one('res.company', 'Company', select=1, help='Let this field empty if this location is shared between all companies'),
213         'scrap_location': fields.boolean('Scrap Location', help='Check this box to allow using this location to put scrapped/damaged goods.'),
214         'valuation_in_account_id': fields.many2one('account.account', 'Stock Valuation Account (Incoming)', domain = [('type','=','other')],
215                                                    help="Used for real-time inventory valuation. When set on a virtual location (non internal type), "
216                                                         "this account will be used to hold the value of products being moved from an internal location "
217                                                         "into this location, instead of the generic Stock Output Account set on the product. "
218                                                         "This has no effect for internal locations."),
219         'valuation_out_account_id': fields.many2one('account.account', 'Stock Valuation Account (Outgoing)', domain = [('type','=','other')],
220                                                    help="Used for real-time inventory valuation. When set on a virtual location (non internal type), "
221                                                         "this account will be used to hold the value of products being moved out of this location "
222                                                         "and into an internal location, instead of the generic Stock Output Account set on the product. "
223                                                         "This has no effect for internal locations."),
224     }
225     _defaults = {
226         'active': True,
227         'usage': 'internal',
228         'chained_location_type': 'none',
229         'chained_auto_packing': 'manual',
230         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.location', context=c),
231         'posx': 0,
232         'posy': 0,
233         'posz': 0,
234         'icon': False,
235         'scrap_location': False,
236     }
237
238     def chained_location_get(self, cr, uid, location, partner=None, product=None, context=None):
239         """ Finds chained location
240         @param location: Location id
241         @param partner: Partner id
242         @param product: Product id
243         @return: List of values
244         """
245         result = None
246         if location.chained_location_type == 'customer':
247             if partner:
248                 result = partner.property_stock_customer
249         elif location.chained_location_type == 'fixed':
250             result = location.chained_location_id
251         if result:
252             return result, location.chained_auto_packing, location.chained_delay, location.chained_journal_id and location.chained_journal_id.id or False, location.chained_company_id and location.chained_company_id.id or False, location.chained_picking_type
253         return result
254
255     def picking_type_get(self, cr, uid, from_location, to_location, context=None):
256         """ Gets type of picking.
257         @param from_location: Source location
258         @param to_location: Destination location
259         @return: Location type
260         """
261         result = 'internal'
262         if (from_location.usage=='internal') and (to_location and to_location.usage in ('customer', 'supplier')):
263             result = 'out'
264         elif (from_location.usage in ('supplier', 'customer')) and (to_location.usage == 'internal'):
265             result = 'in'
266         return result
267
268     def _product_get_all_report(self, cr, uid, ids, product_ids=False, context=None):
269         return self._product_get_report(cr, uid, ids, product_ids, context, recursive=True)
270
271     def _product_get_report(self, cr, uid, ids, product_ids=False,
272             context=None, recursive=False):
273         """ Finds the product quantity and price for particular location.
274         @param product_ids: Ids of product
275         @param recursive: True or False
276         @return: Dictionary of values
277         """
278         if context is None:
279             context = {}
280         product_obj = self.pool.get('product.product')
281         # Take the user company and pricetype
282         context['currency_id'] = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.currency_id.id
283
284         # To be able to offer recursive or non-recursive reports we need to prevent recursive quantities by default
285         context['compute_child'] = False
286
287         if not product_ids:
288             product_ids = product_obj.search(cr, uid, [], context={'active_test': False})
289
290         products = product_obj.browse(cr, uid, product_ids, context=context)
291         products_by_uom = {}
292         products_by_id = {}
293         for product in products:
294             products_by_uom.setdefault(product.uom_id.id, [])
295             products_by_uom[product.uom_id.id].append(product)
296             products_by_id.setdefault(product.id, [])
297             products_by_id[product.id] = product
298
299         result = {}
300         result['product'] = []
301         for id in ids:
302             quantity_total = 0.0
303             total_price = 0.0
304             for uom_id in products_by_uom.keys():
305                 fnc = self._product_get
306                 if recursive:
307                     fnc = self._product_all_get
308                 ctx = context.copy()
309                 ctx['uom'] = uom_id
310                 qty = fnc(cr, uid, id, [x.id for x in products_by_uom[uom_id]],
311                         context=ctx)
312                 for product_id in qty.keys():
313                     if not qty[product_id]:
314                         continue
315                     product = products_by_id[product_id]
316                     quantity_total += qty[product_id]
317
318                     # Compute based on pricetype
319                     # Choose the right filed standard_price to read
320                     amount_unit = product.price_get('standard_price', context=context)[product.id]
321                     price = qty[product_id] * amount_unit
322
323                     total_price += price
324                     result['product'].append({
325                         'price': amount_unit,
326                         'prod_name': product.name,
327                         'code': product.default_code, # used by lot_overview_all report!
328                         'variants': product.variants or '',
329                         'uom': product.uom_id.name,
330                         'prod_qty': qty[product_id],
331                         'price_value': price,
332                     })
333         result['total'] = quantity_total
334         result['total_price'] = total_price
335         return result
336
337     def _product_get_multi_location(self, cr, uid, ids, product_ids=False, context=None,
338                                     states=['done'], what=('in', 'out')):
339         """
340         @param product_ids: Ids of product
341         @param states: List of states
342         @param what: Tuple of
343         @return:
344         """
345         product_obj = self.pool.get('product.product')
346         if context is None:
347             context = {}
348         context.update({
349             'states': states,
350             'what': what,
351             'location': ids
352         })
353         return product_obj.get_product_available(cr, uid, product_ids, context=context)
354
355     def _product_get(self, cr, uid, id, product_ids=False, context=None, states=['done']):
356         """
357         @param product_ids:
358         @param states:
359         @return:
360         """
361         ids = id and [id] or []
362         return self._product_get_multi_location(cr, uid, ids, product_ids, context=context, states=states)
363
364     def _product_all_get(self, cr, uid, id, product_ids=False, context=None, states=['done']):
365         # build the list of ids of children of the location given by id
366         ids = id and [id] or []
367         location_ids = self.search(cr, uid, [('location_id', 'child_of', ids)])
368         return self._product_get_multi_location(cr, uid, location_ids, product_ids, context, states)
369
370     def _product_virtual_get(self, cr, uid, id, product_ids=False, context=None, states=['done']):
371         return self._product_all_get(cr, uid, id, product_ids, context, ['confirmed', 'waiting', 'assigned', 'done'])
372
373     def _product_reserve(self, cr, uid, ids, product_id, product_qty, context=None, lock=False):
374         """
375         Attempt to find a quantity ``product_qty`` (in the product's default uom or the uom passed in ``context``) of product ``product_id``
376         in locations with id ``ids`` and their child locations. If ``lock`` is True, the stock.move lines
377         of product with id ``product_id`` in the searched location will be write-locked using Postgres's
378         "FOR UPDATE NOWAIT" option until the transaction is committed or rolled back, to prevent reservin
379         twice the same products.
380         If ``lock`` is True and the lock cannot be obtained (because another transaction has locked some of
381         the same stock.move lines), a log line will be output and False will be returned, as if there was
382         not enough stock.
383
384         :param product_id: Id of product to reserve
385         :param product_qty: Quantity of product to reserve (in the product's default uom or the uom passed in ``context``)
386         :param lock: if True, the stock.move lines of product with id ``product_id`` in all locations (and children locations) with ``ids`` will
387                      be write-locked using postgres's "FOR UPDATE NOWAIT" option until the transaction is committed or rolled back. This is
388                      to prevent reserving twice the same products.
389         :param context: optional context dictionary: it a 'uom' key is present it will be used instead of the default product uom to
390                         compute the ``product_qty`` and in the return value.
391         :return: List of tuples in the form (qty, location_id) with the (partial) quantities that can be taken in each location to
392                  reach the requested product_qty (``qty`` is expressed in the default uom of the product), of False if enough
393                  products could not be found, or the lock could not be obtained (and ``lock`` was True).
394         """
395         result = []
396         amount = 0.0
397         if context is None:
398             context = {}
399         for id in self.search(cr, uid, [('location_id', 'child_of', ids)]):
400             if lock:
401                 try:
402                     # Must lock with a separate select query because FOR UPDATE can't be used with
403                     # aggregation/group by's (when individual rows aren't identifiable).
404                     # We use a SAVEPOINT to be able to rollback this part of the transaction without
405                     # failing the whole transaction in case the LOCK cannot be acquired.
406                     cr.execute("SAVEPOINT stock_location_product_reserve")
407                     cr.execute("""SELECT id FROM stock_move
408                                   WHERE product_id=%s AND
409                                           (
410                                             (location_dest_id=%s AND
411                                              location_id<>%s AND
412                                              state='done')
413                                             OR
414                                             (location_id=%s AND
415                                              location_dest_id<>%s AND
416                                              state in ('done', 'assigned'))
417                                           )
418                                   FOR UPDATE of stock_move NOWAIT""", (product_id, id, id, id, id), log_exceptions=False)
419                 except Exception:
420                     # Here it's likely that the FOR UPDATE NOWAIT failed to get the LOCK,
421                     # so we ROLLBACK to the SAVEPOINT to restore the transaction to its earlier
422                     # state, we return False as if the products were not available, and log it:
423                     cr.execute("ROLLBACK TO stock_location_product_reserve")
424                     logger = logging.getLogger('stock.location')
425                     logger.warn("Failed attempt to reserve %s x product %s, likely due to another transaction already in progress. Next attempt is likely to work. Detailed error available at DEBUG level.", product_qty, product_id)
426                     logger.debug("Trace of the failed product reservation attempt: ", exc_info=True)
427                     return False
428
429             # XXX TODO: rewrite this with one single query, possibly even the quantity conversion
430             cr.execute("""SELECT product_uom, sum(product_qty) AS product_qty
431                           FROM stock_move
432                           WHERE location_dest_id=%s AND
433                                 location_id<>%s AND
434                                 product_id=%s AND
435                                 state='done'
436                           GROUP BY product_uom
437                        """,
438                        (id, id, product_id))
439             results = cr.dictfetchall()
440             cr.execute("""SELECT product_uom,-sum(product_qty) AS product_qty
441                           FROM stock_move
442                           WHERE location_id=%s AND
443                                 location_dest_id<>%s AND
444                                 product_id=%s AND
445                                 state in ('done', 'assigned')
446                           GROUP BY product_uom
447                        """,
448                        (id, id, product_id))
449             results += cr.dictfetchall()
450             total = 0.0
451             results2 = 0.0
452             for r in results:
453                 amount = self.pool.get('product.uom')._compute_qty(cr, uid, r['product_uom'], r['product_qty'], context.get('uom', False))
454                 results2 += amount
455                 total += amount
456             if total <= 0.0:
457                 continue
458
459             amount = results2
460             if amount > 0:
461                 if amount > min(total, product_qty):
462                     amount = min(product_qty, total)
463                 result.append((amount, id))
464                 product_qty -= amount
465                 total -= amount
466                 if product_qty <= 0.0:
467                     return result
468                 if total <= 0.0:
469                     continue
470         return False
471
472 stock_location()
473
474
475 class stock_tracking(osv.osv):
476     _name = "stock.tracking"
477     _description = "Packs"
478
479     def checksum(sscc):
480         salt = '31' * 8 + '3'
481         sum = 0
482         for sscc_part, salt_part in zip(sscc, salt):
483             sum += int(sscc_part) * int(salt_part)
484         return (10 - (sum % 10)) % 10
485     checksum = staticmethod(checksum)
486
487     def make_sscc(self, cr, uid, context=None):
488         sequence = self.pool.get('ir.sequence').get(cr, uid, 'stock.lot.tracking')
489         try:
490             return sequence + str(self.checksum(sequence))
491         except Exception:
492             return sequence
493
494     _columns = {
495         'name': fields.char('Pack Reference', size=64, required=True, select=True, help="By default, the pack reference is generated following the sscc standard. (Serial number + 1 check digit)"),
496         'active': fields.boolean('Active', help="By unchecking the active field, you may hide a pack without deleting it."),
497         'serial': fields.char('Additional Reference', size=64, select=True, help="Other reference or serial number"),
498         'move_ids': fields.one2many('stock.move', 'tracking_id', 'Moves for this pack', readonly=True),
499         'date': fields.datetime('Creation Date', required=True),
500     }
501     _defaults = {
502         'active': 1,
503         'name': make_sscc,
504         'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
505     }
506
507     def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=100):
508         if not args:
509             args = []
510         ids = self.search(cr, user, [('serial', '=', name)]+ args, limit=limit, context=context)
511         ids += self.search(cr, user, [('name', operator, name)]+ args, limit=limit, context=context)
512         return self.name_get(cr, user, ids, context)
513
514     def name_get(self, cr, uid, ids, context=None):
515         if not len(ids):
516             return []
517         res = [(r['id'], r['name']+' ['+(r['serial'] or '')+']') for r in self.read(cr, uid, ids, ['name', 'serial'], context)]
518         return res
519
520     def unlink(self, cr, uid, ids, context=None):
521         raise osv.except_osv(_('Error'), _('You can not remove a lot line !'))
522
523     def action_traceability(self, cr, uid, ids, context={}):
524         """ It traces the information of a product
525         @param self: The object pointer.
526         @param cr: A database cursor
527         @param uid: ID of the user currently logged in
528         @param ids: List of IDs selected
529         @param context: A standard dictionary
530         @return: A dictionary of values
531         """
532         value={}
533         value=self.pool.get('action.traceability').action_traceability(cr,uid,ids,context)
534         return value
535 stock_tracking()
536
537 #----------------------------------------------------------
538 # Stock Picking
539 #----------------------------------------------------------
540 class stock_picking(osv.osv):
541     _name = "stock.picking"
542     _description = "Picking List"
543
544     def _set_maximum_date(self, cr, uid, ids, name, value, arg, context=None):
545         """ Calculates planned date if it is greater than 'value'.
546         @param name: Name of field
547         @param value: Value of field
548         @param arg: User defined argument
549         @return: True or False
550         """
551         if not value:
552             return False
553         if isinstance(ids, (int, long)):
554             ids = [ids]
555         for pick in self.browse(cr, uid, ids, context=context):
556             sql_str = """update stock_move set
557                     date='%s'
558                 where
559                     picking_id=%d """ % (value, pick.id)
560
561             if pick.max_date:
562                 sql_str += " and (date='" + pick.max_date + "' or date>'" + value + "')"
563             cr.execute(sql_str)
564         return True
565
566     def _set_minimum_date(self, cr, uid, ids, name, value, arg, context=None):
567         """ Calculates planned date if it is less than 'value'.
568         @param name: Name of field
569         @param value: Value of field
570         @param arg: User defined argument
571         @return: True or False
572         """
573         if not value:
574             return False
575         if isinstance(ids, (int, long)):
576             ids = [ids]
577         for pick in self.browse(cr, uid, ids, context=context):
578             sql_str = """update stock_move set
579                     date='%s'
580                 where
581                     picking_id=%s """ % (value, pick.id)
582             if pick.min_date:
583                 sql_str += " and (date='" + pick.min_date + "' or date<'" + value + "')"
584             cr.execute(sql_str)
585         return True
586
587     def get_min_max_date(self, cr, uid, ids, field_name, arg, context=None):
588         """ Finds minimum and maximum dates for picking.
589         @return: Dictionary of values
590         """
591         res = {}
592         for id in ids:
593             res[id] = {'min_date': False, 'max_date': False}
594         if not ids:
595             return res
596         cr.execute("""select
597                 picking_id,
598                 min(date_expected),
599                 max(date_expected)
600             from
601                 stock_move
602             where
603                 picking_id IN %s
604             group by
605                 picking_id""",(tuple(ids),))
606         for pick, dt1, dt2 in cr.fetchall():
607             res[pick]['min_date'] = dt1
608             res[pick]['max_date'] = dt2
609         return res
610
611     def create(self, cr, user, vals, context=None):
612         if ('name' not in vals) or (vals.get('name')=='/'):
613             seq_obj_name =  'stock.picking.' + vals['type']
614             vals['name'] = self.pool.get('ir.sequence').get(cr, user, seq_obj_name)
615         new_id = super(stock_picking, self).create(cr, user, vals, context)
616         return new_id
617
618     _columns = {
619         'name': fields.char('Reference', size=64, select=True),
620         'origin': fields.char('Origin', size=64, help="Reference of the document that produced this picking.", select=True),
621         'backorder_id': fields.many2one('stock.picking', 'Back Order of', help="If this picking was split this field links to the picking that contains the other part that has been processed already.", select=True),
622         'type': fields.selection([('out', 'Sending Goods'), ('in', 'Getting Goods'), ('internal', 'Internal')], 'Shipping Type', required=True, select=True, help="Shipping type specify, goods coming in or going out."),
623         'note': fields.text('Notes'),
624         'stock_journal_id': fields.many2one('stock.journal','Stock Journal', select=True),
625         'location_id': fields.many2one('stock.location', 'Location', help="Keep empty if you produce at the location where the finished products are needed." \
626                 "Set a location if you produce at a fixed location. This can be a partner location " \
627                 "if you subcontract the manufacturing operations.", select=True),
628         'location_dest_id': fields.many2one('stock.location', 'Dest. Location',help="Location where the system will stock the finished products.", select=True),
629         'move_type': fields.selection([('direct', 'Partial Delivery'), ('one', 'All at once')], 'Delivery Method', required=True, help="It specifies goods to be delivered all at once or by direct delivery"),
630         'state': fields.selection([
631             ('draft', 'New'),
632             ('auto', 'Waiting Another Operation'),
633             ('confirmed', 'Waiting Availability'),
634             ('assigned', 'Ready to Process'),
635             ('done', 'Done'),
636             ('cancel', 'Cancelled'),
637             ], 'State', readonly=True, select=True,
638             help="* Draft: not confirmed yet and will not be scheduled until confirmed\n"\
639                  "* Confirmed: still waiting for the availability of products\n"\
640                  "* Available: products reserved, simply waiting for confirmation.\n"\
641                  "* Waiting: waiting for another move to proceed before it becomes automatically available (e.g. in Make-To-Order flows)\n"\
642                  "* Done: has been processed, can't be modified or cancelled anymore\n"\
643                  "* Cancelled: has been cancelled, can't be confirmed anymore"),
644         'min_date': fields.function(get_min_max_date, fnct_inv=_set_minimum_date, multi="min_max_date",
645                  store=True, type='datetime', string='Expected Date', select=1, help="Expected date for the picking to be processed"),
646         'date': fields.datetime('Order Date', help="Date of Order", select=True),
647         'date_done': fields.datetime('Date Done', help="Date of Completion"),
648         'max_date': fields.function(get_min_max_date, fnct_inv=_set_maximum_date, multi="min_max_date",
649                  store=True, type='datetime', string='Max. Expected Date', select=2),
650         'move_lines': fields.one2many('stock.move', 'picking_id', 'Internal Moves', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}),
651         'auto_picking': fields.boolean('Auto-Picking'),
652         'address_id': fields.many2one('res.partner.address', 'Address', help="Address of partner"),
653         'partner_id': fields.related('address_id','partner_id',type='many2one',relation='res.partner',string='Partner',store=True),
654         'invoice_state': fields.selection([
655             ("invoiced", "Invoiced"),
656             ("2binvoiced", "To Be Invoiced"),
657             ("none", "Not Applicable")], "Invoice Control",
658             select=True, required=True, readonly=True, states={'draft': [('readonly', False)]}),
659         'company_id': fields.many2one('res.company', 'Company', required=True, select=True),
660     }
661     _defaults = {
662         'name': lambda self, cr, uid, context: '/',
663         'state': 'draft',
664         'move_type': 'direct',
665         'type': 'in',
666         'invoice_state': 'none',
667         'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
668         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.picking', context=c)
669     }
670     _sql_constraints = [
671         ('name_uniq', 'unique(name, company_id)', 'Reference must be unique per Company!'),
672     ]
673
674     def action_process(self, cr, uid, ids, context=None):
675         if context is None: context = {}
676         context = dict(context, active_ids=ids, active_model=self._name)
677         partial_id = self.pool.get("stock.partial.picking").create(cr, uid, {}, context=context)
678         return {
679             'name':_("Products to Process"),
680             'view_mode': 'form',
681             'view_id': False,
682             'view_type': 'form',
683             'res_model': 'stock.partial.picking',
684             'res_id': partial_id,
685             'type': 'ir.actions.act_window',
686             'nodestroy': True,
687             'target': 'new',
688             'domain': '[]',
689             'context': context,
690         }
691
692     def copy(self, cr, uid, id, default=None, context=None):
693         if default is None:
694             default = {}
695         default = default.copy()
696         picking_obj = self.browse(cr, uid, id, context=context)
697         move_obj=self.pool.get('stock.move')
698         if ('name' not in default) or (picking_obj.name=='/'):
699             seq_obj_name =  'stock.picking.' + picking_obj.type
700             default['name'] = self.pool.get('ir.sequence').get(cr, uid, seq_obj_name)
701             default['origin'] = ''
702             default['backorder_id'] = False
703         if picking_obj.invoice_state == 'invoiced':
704             default['invoice_state'] = '2binvoiced'
705         res=super(stock_picking, self).copy(cr, uid, id, default, context)
706         if res:
707             picking_obj = self.browse(cr, uid, res, context=context)
708             for move in picking_obj.move_lines:
709                 move_obj.write(cr, uid, [move.id], {'tracking_id': False,'prodlot_id':False, 'move_history_ids2': [(6, 0, [])], 'move_history_ids': [(6, 0, [])]})
710         return res
711
712     def onchange_partner_in(self, cr, uid, context=None, partner_id=None):
713         return {}
714
715     def action_explode(self, cr, uid, moves, context=None):
716         return moves
717
718     def action_confirm(self, cr, uid, ids, context=None):
719         """ Confirms picking.
720         @return: True
721         """
722         self.write(cr, uid, ids, {'state': 'confirmed'})
723         todo = []
724         for picking in self.browse(cr, uid, ids, context=context):
725             for r in picking.move_lines:
726                 if r.state == 'draft':
727                     todo.append(r.id)
728
729         self.log_picking(cr, uid, ids, context=context)
730
731         todo = self.action_explode(cr, uid, todo, context)
732         if len(todo):
733             self.pool.get('stock.move').action_confirm(cr, uid, todo, context=context)
734         return True
735
736     def test_auto_picking(self, cr, uid, ids):
737         # TODO: Check locations to see if in the same location ?
738         return True
739
740     def action_assign(self, cr, uid, ids, *args):
741         """ Changes state of picking to available if all moves are confirmed.
742         @return: True
743         """
744         for pick in self.browse(cr, uid, ids):
745             move_ids = [x.id for x in pick.move_lines if x.state == 'confirmed']
746             if not move_ids:
747                 raise osv.except_osv(_('Warning !'),_('Not enough stock, unable to reserve the products.'))
748             self.pool.get('stock.move').action_assign(cr, uid, move_ids)
749         return True
750
751     def force_assign(self, cr, uid, ids, *args):
752         """ Changes state of picking to available if moves are confirmed or waiting.
753         @return: True
754         """
755         wf_service = netsvc.LocalService("workflow")
756         for pick in self.browse(cr, uid, ids):
757             move_ids = [x.id for x in pick.move_lines if x.state in ['confirmed','waiting']]
758             self.pool.get('stock.move').force_assign(cr, uid, move_ids)
759             wf_service.trg_write(uid, 'stock.picking', pick.id, cr)
760         return True
761
762     def draft_force_assign(self, cr, uid, ids, *args):
763         """ Confirms picking directly from draft state.
764         @return: True
765         """
766         wf_service = netsvc.LocalService("workflow")
767         for pick in self.browse(cr, uid, ids):
768             if not pick.move_lines:
769                 raise osv.except_osv(_('Error !'),_('You can not process picking without stock moves'))
770             wf_service.trg_validate(uid, 'stock.picking', pick.id,
771                 'button_confirm', cr)
772         return True
773
774     def draft_validate(self, cr, uid, ids, context=None):
775         """ Validates picking directly from draft state.
776         @return: True
777         """
778         wf_service = netsvc.LocalService("workflow")
779         self.draft_force_assign(cr, uid, ids)
780         for pick in self.browse(cr, uid, ids, context=context):
781             move_ids = [x.id for x in pick.move_lines]
782             self.pool.get('stock.move').force_assign(cr, uid, move_ids)
783             wf_service.trg_write(uid, 'stock.picking', pick.id, cr)
784         return self.action_process(
785             cr, uid, ids, context=context)
786     def cancel_assign(self, cr, uid, ids, *args):
787         """ Cancels picking and moves.
788         @return: True
789         """
790         wf_service = netsvc.LocalService("workflow")
791         for pick in self.browse(cr, uid, ids):
792             move_ids = [x.id for x in pick.move_lines]
793             self.pool.get('stock.move').cancel_assign(cr, uid, move_ids)
794             wf_service.trg_write(uid, 'stock.picking', pick.id, cr)
795         return True
796
797     def action_assign_wkf(self, cr, uid, ids, context=None):
798         """ Changes picking state to assigned.
799         @return: True
800         """
801         self.write(cr, uid, ids, {'state': 'assigned'})
802         self.log_picking(cr, uid, ids, context=context)
803         return True
804
805     def test_finished(self, cr, uid, ids):
806         """ Tests whether the move is in done or cancel state or not.
807         @return: True or False
808         """
809         move_ids = self.pool.get('stock.move').search(cr, uid, [('picking_id', 'in', ids)])
810         for move in self.pool.get('stock.move').browse(cr, uid, move_ids):
811             if move.state not in ('done', 'cancel'):
812
813                 if move.product_qty != 0.0:
814                     return False
815                 else:
816                     move.write({'state': 'done'})
817         return True
818
819     def test_assigned(self, cr, uid, ids):
820         """ Tests whether the move is in assigned state or not.
821         @return: True or False
822         """
823         #TOFIX: assignment of move lines should be call before testing assigment otherwise picking never gone in assign state
824         ok = True
825         for pick in self.browse(cr, uid, ids):
826             mt = pick.move_type
827             for move in pick.move_lines:
828                 if (move.state in ('confirmed', 'draft')) and (mt == 'one'):
829                     return False
830                 if (mt == 'direct') and (move.state == 'assigned') and (move.product_qty):
831                     return True
832                 ok = ok and (move.state in ('cancel', 'done', 'assigned'))
833         return ok
834
835     def action_cancel(self, cr, uid, ids, context=None):
836         """ Changes picking state to cancel.
837         @return: True
838         """
839         for pick in self.browse(cr, uid, ids, context=context):
840             ids2 = [move.id for move in pick.move_lines]
841             self.pool.get('stock.move').action_cancel(cr, uid, ids2, context)
842         self.write(cr, uid, ids, {'state': 'cancel', 'invoice_state': 'none'})
843         self.log_picking(cr, uid, ids, context=context)
844         return True
845
846     #
847     # TODO: change and create a move if not parents
848     #
849     def action_done(self, cr, uid, ids, context=None):
850         """ Changes picking state to done.
851         @return: True
852         """
853         self.write(cr, uid, ids, {'state': 'done', 'date_done': time.strftime('%Y-%m-%d %H:%M:%S')})
854         return True
855
856     def action_move(self, cr, uid, ids, context=None):
857         """ Changes move state to assigned.
858         @return: True
859         """
860         for pick in self.browse(cr, uid, ids, context=context):
861             todo = []
862             for move in pick.move_lines:
863                 if move.state == 'draft':
864                     self.pool.get('stock.move').action_confirm(cr, uid, [move.id],
865                         context=context)
866                     todo.append(move.id)
867                 elif move.state in ('assigned','confirmed'):
868                     todo.append(move.id)
869             if len(todo):
870                 self.pool.get('stock.move').action_done(cr, uid, todo,
871                         context=context)
872         return True
873
874     def get_currency_id(self, cr, uid, picking):
875         return False
876
877     def _get_partner_to_invoice(self, cr, uid, picking, context=None):
878         """ Gets the partner that will be invoiced
879         Note that this function is inherited in the sale module
880         @return: partner object
881         """
882         return picking.address_id and picking.address_id.partner_id
883
884     def _get_payment_term(self, cr, uid, picking):
885         """ Gets payment term from partner.
886         @return: Payment term
887         """
888         partner = picking.address_id.partner_id
889         return partner.property_payment_term and partner.property_payment_term.id or False
890
891     def _get_address_invoice(self, cr, uid, picking):
892         """ Gets invoice address of a partner
893         @return {'contact': address, 'invoice': address} for invoice
894         """
895         partner_obj = self.pool.get('res.partner')
896         partner = picking.address_id.partner_id
897         return partner_obj.address_get(cr, uid, [partner.id],
898                 ['contact', 'invoice'])
899
900     def _get_comment_invoice(self, cr, uid, picking):
901         """
902         @return: comment string for invoice
903         """
904         return picking.note or ''
905
906     def _get_price_unit_invoice(self, cr, uid, move_line, type, context=None):
907         """ Gets price unit for invoice
908         @param move_line: Stock move lines
909         @param type: Type of invoice
910         @return: The price unit for the move line
911         """
912         if context is None:
913             context = {}
914
915         if type in ('in_invoice', 'in_refund'):
916             # Take the user company and pricetype
917             context['currency_id'] = move_line.company_id.currency_id.id
918             amount_unit = move_line.product_id.price_get('standard_price', context=context)[move_line.product_id.id]
919             return amount_unit
920         else:
921             return move_line.product_id.list_price
922
923     def _get_discount_invoice(self, cr, uid, move_line):
924         '''Return the discount for the move line'''
925         return 0.0
926
927     def _get_taxes_invoice(self, cr, uid, move_line, type):
928         """ Gets taxes on invoice
929         @param move_line: Stock move lines
930         @param type: Type of invoice
931         @return: Taxes Ids for the move line
932         """
933         if type in ('in_invoice', 'in_refund'):
934             taxes = move_line.product_id.supplier_taxes_id
935         else:
936             taxes = move_line.product_id.taxes_id
937
938         if move_line.picking_id and move_line.picking_id.address_id and move_line.picking_id.address_id.partner_id:
939             return self.pool.get('account.fiscal.position').map_tax(
940                 cr,
941                 uid,
942                 move_line.picking_id.address_id.partner_id.property_account_position,
943                 taxes
944             )
945         else:
946             return map(lambda x: x.id, taxes)
947
948     def _get_account_analytic_invoice(self, cr, uid, picking, move_line):
949         return False
950
951     def _invoice_line_hook(self, cr, uid, move_line, invoice_line_id):
952         '''Call after the creation of the invoice line'''
953         return
954
955     def _invoice_hook(self, cr, uid, picking, invoice_id):
956         '''Call after the creation of the invoice'''
957         return
958
959     def _get_invoice_type(self, pick):
960         src_usage = dest_usage = None
961         inv_type = None
962         if pick.invoice_state == '2binvoiced':
963             if pick.move_lines:
964                 src_usage = pick.move_lines[0].location_id.usage
965                 dest_usage = pick.move_lines[0].location_dest_id.usage
966             if pick.type == 'out' and dest_usage == 'supplier':
967                 inv_type = 'in_refund'
968             elif pick.type == 'out' and dest_usage == 'customer':
969                 inv_type = 'out_invoice'
970             elif pick.type == 'in' and src_usage == 'supplier':
971                 inv_type = 'in_invoice'
972             elif pick.type == 'in' and src_usage == 'customer':
973                 inv_type = 'out_refund'
974             else:
975                 inv_type = 'out_invoice'
976         return inv_type
977
978     def _prepare_invoice_group(self, cr, uid, picking, partner, invoice, context=None):
979         """Builds the dict for grouped invoices"""
980         comment = self._get_comment_invoice(cr, uid, picking)
981
982         return {
983             'name': (invoice.name or '') + ', ' + (picking.name or ''),
984             'origin': (invoice.origin or '') + ', ' + (picking.name or '') + (picking.origin and (':' + picking.origin) or ''),
985             'comment': (comment and (invoice.comment and invoice.comment + "\n" + comment or comment)) or (invoice.comment and invoice.comment or ''),
986             'date_invoice': context.get('date_inv', False),
987             'user_id': uid
988         }
989
990     def _prepare_invoice(self, cr, uid, picking, partner, inv_type, journal_id, context=None):
991         """Builds the dict containing the values for the invoice"""
992         if inv_type in ('out_invoice', 'out_refund'):
993             account_id = partner.property_account_receivable.id
994         else:
995             account_id = partner.property_account_payable.id
996         address_contact_id, address_invoice_id = \
997                 self._get_address_invoice(cr, uid, picking).values()
998         comment = self._get_comment_invoice(cr, uid, picking)
999         invoice_vals = {
1000             'name': picking.name,
1001             'origin': (picking.name or '') + (picking.origin and (':' + picking.origin) or ''),
1002             'type': inv_type,
1003             'account_id': account_id,
1004             'partner_id': partner.id,
1005             'address_invoice_id': address_invoice_id,
1006             'address_contact_id': address_contact_id,
1007             'comment': comment,
1008             'payment_term': self._get_payment_term(cr, uid, picking),
1009             'fiscal_position': partner.property_account_position.id,
1010             'date_invoice': context.get('date_inv', False),
1011             'company_id': picking.company_id.id,
1012             'user_id': uid
1013         }
1014         cur_id = self.get_currency_id(cr, uid, picking)
1015         if cur_id:
1016             invoice_vals['currency_id'] = cur_id
1017         if journal_id:
1018             invoice_vals['journal_id'] = journal_id
1019         return invoice_vals
1020
1021     def action_invoice_create(self, cr, uid, ids, journal_id=False,
1022             group=False, type='out_invoice', context=None):
1023         """ Creates invoice based on the invoice state selected for picking.
1024         @param journal_id: Id of journal
1025         @param group: Whether to create a group invoice or not
1026         @param type: Type invoice to be created
1027         @return: Ids of created invoices for the pickings
1028         """
1029         if context is None:
1030             context = {}
1031
1032         invoice_obj = self.pool.get('account.invoice')
1033         invoice_line_obj = self.pool.get('account.invoice.line')
1034         invoices_group = {}
1035         res = {}
1036         inv_type = type
1037         for picking in self.browse(cr, uid, ids, context=context):
1038             if picking.invoice_state != '2binvoiced':
1039                 continue
1040             partner = self._get_partner_to_invoice(cr, uid, picking, context=context)
1041             if not partner:
1042                 raise osv.except_osv(_('Error, no partner !'),
1043                     _('Please put a partner on the picking list if you want to generate invoice.'))
1044
1045             if not inv_type:
1046                 inv_type = self._get_invoice_type(picking)
1047
1048             if group and partner.id in invoices_group:
1049                 invoice_id = invoices_group[partner.id]
1050                 invoice = invoice_obj.browse(cr, uid, invoice_id)
1051                 invoice_obj.write(cr, uid, [invoice_id],
1052                         self._prepare_invoice_group(cr, uid, picking, partner, invoice, context=context),
1053                         context=context)
1054             else:
1055                 invoice_id = invoice_obj.create(cr, uid,
1056                         self._prepare_invoice(cr, uid, picking, partner, inv_type, journal_id, context=context),
1057                         context=context)
1058                 invoices_group[partner.id] = invoice_id
1059             res[picking.id] = invoice_id
1060             for move_line in picking.move_lines:
1061                 if move_line.state == 'cancel':
1062                     continue
1063                 origin = move_line.picking_id.name or ''
1064                 if move_line.picking_id.origin:
1065                     origin += ':' + move_line.picking_id.origin
1066                 if group:
1067                     name = (picking.name or '') + '-' + move_line.name
1068                 else:
1069                     name = move_line.name
1070
1071                 if inv_type in ('out_invoice', 'out_refund'):
1072                     account_id = move_line.product_id.product_tmpl_id.\
1073                             property_account_income.id
1074                     if not account_id:
1075                         account_id = move_line.product_id.categ_id.\
1076                                 property_account_income_categ.id
1077                 else:
1078                     account_id = move_line.product_id.product_tmpl_id.\
1079                             property_account_expense.id
1080                     if not account_id:
1081                         account_id = move_line.product_id.categ_id.\
1082                                 property_account_expense_categ.id
1083
1084                 price_unit = self._get_price_unit_invoice(cr, uid,
1085                         move_line, inv_type)
1086                 discount = self._get_discount_invoice(cr, uid, move_line)
1087                 tax_ids = self._get_taxes_invoice(cr, uid, move_line, inv_type)
1088                 account_analytic_id = self._get_account_analytic_invoice(cr, uid, picking, move_line)
1089
1090                 #set UoS if it's a sale and the picking doesn't have one
1091                 uos_id = move_line.product_uos and move_line.product_uos.id or False
1092                 if not uos_id and inv_type in ('out_invoice', 'out_refund'):
1093                     uos_id = move_line.product_uom.id
1094
1095                 account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, partner.property_account_position, account_id)
1096                 invoice_line_id = invoice_line_obj.create(cr, uid, {
1097                     'name': name,
1098                     'origin': origin,
1099                     'invoice_id': invoice_id,
1100                     'uos_id': uos_id,
1101                     'product_id': move_line.product_id.id,
1102                     'account_id': account_id,
1103                     'price_unit': price_unit,
1104                     'discount': discount,
1105                     'quantity': move_line.product_uos_qty or move_line.product_qty,
1106                     'invoice_line_tax_id': [(6, 0, tax_ids)],
1107                     'account_analytic_id': account_analytic_id,
1108                 }, context=context)
1109                 self._invoice_line_hook(cr, uid, move_line, invoice_line_id)
1110
1111             invoice_obj.button_compute(cr, uid, [invoice_id], context=context,
1112                     set_total=(inv_type in ('in_invoice', 'in_refund')))
1113             self.write(cr, uid, [picking.id], {
1114                 'invoice_state': 'invoiced',
1115                 }, context=context)
1116             self._invoice_hook(cr, uid, picking, invoice_id)
1117         self.write(cr, uid, res.keys(), {
1118             'invoice_state': 'invoiced',
1119             }, context=context)
1120         return res
1121
1122     def test_done(self, cr, uid, ids, context=None):
1123         """ Test whether the move lines are done or not.
1124         @return: True or False
1125         """
1126         ok = False
1127         for pick in self.browse(cr, uid, ids, context=context):
1128             if not pick.move_lines:
1129                 return True
1130             for move in pick.move_lines:
1131                 if move.state not in ('cancel','done'):
1132                     return False
1133                 if move.state=='done':
1134                     ok = True
1135         return ok
1136
1137     def test_cancel(self, cr, uid, ids, context=None):
1138         """ Test whether the move lines are canceled or not.
1139         @return: True or False
1140         """
1141         for pick in self.browse(cr, uid, ids, context=context):
1142             for move in pick.move_lines:
1143                 if move.state not in ('cancel',):
1144                     return False
1145         return True
1146
1147     def allow_cancel(self, cr, uid, ids, context=None):
1148         for pick in self.browse(cr, uid, ids, context=context):
1149             if not pick.move_lines:
1150                 return True
1151             for move in pick.move_lines:
1152                 if move.state == 'done':
1153                     raise osv.except_osv(_('Error'), _('You cannot cancel picking because stock move is in done state !'))
1154         return True
1155     def unlink(self, cr, uid, ids, context=None):
1156         move_obj = self.pool.get('stock.move')
1157         if context is None:
1158             context = {}
1159         for pick in self.browse(cr, uid, ids, context=context):
1160             if pick.state in ['done','cancel']:
1161                 raise osv.except_osv(_('Error'), _('You cannot remove the picking which is in %s state !')%(pick.state,))
1162             else:
1163                 ids2 = [move.id for move in pick.move_lines]
1164                 ctx = context.copy()
1165                 ctx.update({'call_unlink':True})
1166                 if pick.state != 'draft':
1167                     #Cancelling the move in order to affect Virtual stock of product
1168                     move_obj.action_cancel(cr, uid, ids2, ctx)
1169                 #Removing the move
1170                 move_obj.unlink(cr, uid, ids2, ctx)
1171
1172         return super(stock_picking, self).unlink(cr, uid, ids, context=context)
1173
1174     # FIXME: needs refactoring, this code is partially duplicated in stock_move.do_partial()!
1175     def do_partial(self, cr, uid, ids, partial_datas, context=None):
1176         """ Makes partial picking and moves done.
1177         @param partial_datas : Dictionary containing details of partial picking
1178                           like partner_id, address_id, delivery_date,
1179                           delivery moves with product_id, product_qty, uom
1180         @return: Dictionary of values
1181         """
1182         if context is None:
1183             context = {}
1184         else:
1185             context = dict(context)
1186         res = {}
1187         move_obj = self.pool.get('stock.move')
1188         product_obj = self.pool.get('product.product')
1189         currency_obj = self.pool.get('res.currency')
1190         uom_obj = self.pool.get('product.uom')
1191         sequence_obj = self.pool.get('ir.sequence')
1192         wf_service = netsvc.LocalService("workflow")
1193         for pick in self.browse(cr, uid, ids, context=context):
1194             new_picking = None
1195             complete, too_many, too_few = [], [], []
1196             move_product_qty, prodlot_ids, product_avail, partial_qty, product_uoms = {}, {}, {}, {}, {}
1197             for move in pick.move_lines:
1198                 if move.state in ('done', 'cancel'):
1199                     continue
1200                 partial_data = partial_datas.get('move%s'%(move.id), {})
1201                 product_qty = partial_data.get('product_qty',0.0)
1202                 move_product_qty[move.id] = product_qty
1203                 product_uom = partial_data.get('product_uom',False)
1204                 product_price = partial_data.get('product_price',0.0)
1205                 product_currency = partial_data.get('product_currency',False)
1206                 prodlot_id = partial_data.get('prodlot_id')
1207                 prodlot_ids[move.id] = prodlot_id
1208                 product_uoms[move.id] = product_uom
1209                 partial_qty[move.id] = uom_obj._compute_qty(cr, uid, product_uoms[move.id], product_qty, move.product_uom.id)
1210                 if move.product_qty == partial_qty[move.id]:
1211                     complete.append(move)
1212                 elif move.product_qty > partial_qty[move.id]:
1213                     too_few.append(move)
1214                 else:
1215                     too_many.append(move)
1216
1217                 # Average price computation
1218                 if (pick.type == 'in') and (move.product_id.cost_method == 'average'):
1219                     product = product_obj.browse(cr, uid, move.product_id.id)
1220                     move_currency_id = move.company_id.currency_id.id
1221                     context['currency_id'] = move_currency_id
1222                     qty = uom_obj._compute_qty(cr, uid, product_uom, product_qty, product.uom_id.id)
1223
1224                     if product.id in product_avail:
1225                         product_avail[product.id] += qty
1226                     else:
1227                         product_avail[product.id] = product.qty_available
1228
1229                     if qty > 0:
1230                         new_price = currency_obj.compute(cr, uid, product_currency,
1231                                 move_currency_id, product_price)
1232                         new_price = uom_obj._compute_price(cr, uid, product_uom, new_price,
1233                                 product.uom_id.id)
1234                         if product.qty_available <= 0:
1235                             new_std_price = new_price
1236                         else:
1237                             # Get the standard price
1238                             amount_unit = product.price_get('standard_price', context=context)[product.id]
1239                             new_std_price = ((amount_unit * product_avail[product.id])\
1240                                 + (new_price * qty))/(product_avail[product.id] + qty)
1241                         # Write the field according to price type field
1242                         product_obj.write(cr, uid, [product.id], {'standard_price': new_std_price})
1243
1244                         # Record the values that were chosen in the wizard, so they can be
1245                         # used for inventory valuation if real-time valuation is enabled.
1246                         move_obj.write(cr, uid, [move.id],
1247                                 {'price_unit': product_price,
1248                                  'price_currency_id': product_currency})
1249
1250
1251             for move in too_few:
1252                 product_qty = move_product_qty[move.id]
1253                 if not new_picking:
1254                     new_picking = self.copy(cr, uid, pick.id,
1255                             {
1256                                 'name': sequence_obj.get(cr, uid, 'stock.picking.%s'%(pick.type)),
1257                                 'move_lines' : [],
1258                                 'state':'draft',
1259                             })
1260                 if product_qty != 0:
1261                     defaults = {
1262                             'product_qty' : product_qty,
1263                             'product_uos_qty': product_qty, #TODO: put correct uos_qty
1264                             'picking_id' : new_picking,
1265                             'state': 'assigned',
1266                             'move_dest_id': False,
1267                             'price_unit': move.price_unit,
1268                             'product_uom': product_uoms[move.id]
1269                     }
1270                     prodlot_id = prodlot_ids[move.id]
1271                     if prodlot_id:
1272                         defaults.update(prodlot_id=prodlot_id)
1273                     move_obj.copy(cr, uid, move.id, defaults)
1274                 move_obj.write(cr, uid, [move.id],
1275                         {
1276                             'product_qty' : move.product_qty - partial_qty[move.id],
1277                             'product_uos_qty': move.product_qty - partial_qty[move.id], #TODO: put correct uos_qty
1278                             
1279                         })
1280
1281             if new_picking:
1282                 move_obj.write(cr, uid, [c.id for c in complete], {'picking_id': new_picking})
1283             for move in complete:
1284                 defaults = {'product_uom': product_uoms[move.id], 'product_qty': move_product_qty[move.id]}
1285                 if prodlot_ids.get(move.id):
1286                     defaults.update({'prodlot_id': prodlot_ids[move.id]})
1287                 move_obj.write(cr, uid, [move.id], defaults)
1288             for move in too_many:
1289                 product_qty = move_product_qty[move.id]
1290                 defaults = {
1291                     'product_qty' : product_qty,
1292                     'product_uos_qty': product_qty, #TODO: put correct uos_qty
1293                     'product_uom': product_uoms[move.id]
1294                 }
1295                 prodlot_id = prodlot_ids.get(move.id)
1296                 if prodlot_ids.get(move.id):
1297                     defaults.update(prodlot_id=prodlot_id)
1298                 if new_picking:
1299                     defaults.update(picking_id=new_picking)
1300                 move_obj.write(cr, uid, [move.id], defaults)
1301
1302             # At first we confirm the new picking (if necessary)
1303             if new_picking:
1304                 wf_service.trg_validate(uid, 'stock.picking', new_picking, 'button_confirm', cr)
1305                 # Then we finish the good picking
1306                 self.write(cr, uid, [pick.id], {'backorder_id': new_picking})
1307                 self.action_move(cr, uid, [new_picking])
1308                 wf_service.trg_validate(uid, 'stock.picking', new_picking, 'button_done', cr)
1309                 wf_service.trg_write(uid, 'stock.picking', pick.id, cr)
1310                 delivered_pack_id = new_picking
1311             else:
1312                 self.action_move(cr, uid, [pick.id])
1313                 wf_service.trg_validate(uid, 'stock.picking', pick.id, 'button_done', cr)
1314                 delivered_pack_id = pick.id
1315
1316             delivered_pack = self.browse(cr, uid, delivered_pack_id, context=context)
1317             res[pick.id] = {'delivered_picking': delivered_pack.id or False}
1318
1319         return res
1320
1321     def log_picking(self, cr, uid, ids, context=None):
1322         """ This function will create log messages for picking.
1323         @param cr: the database cursor
1324         @param uid: the current user's ID for security checks,
1325         @param ids: List of Picking Ids
1326         @param context: A standard dictionary for contextual values
1327         """
1328         if context is None:
1329             context = {}
1330         data_obj = self.pool.get('ir.model.data')
1331         for pick in self.browse(cr, uid, ids, context=context):
1332             msg=''
1333             if pick.auto_picking:
1334                 continue
1335             type_list = {
1336                 'out':_("Delivery Order"),
1337                 'in':_('Reception'),
1338                 'internal': _('Internal picking'),
1339             }
1340             view_list = {
1341                 'out': 'view_picking_out_form',
1342                 'in': 'view_picking_in_form',
1343                 'internal': 'view_picking_form',
1344             }
1345             message = type_list.get(pick.type, _('Document')) + " '" + (pick.name or '?') + "' "
1346             if pick.min_date:
1347                 msg= _(' for the ')+ datetime.strptime(pick.min_date, '%Y-%m-%d %H:%M:%S').strftime('%m/%d/%Y')
1348             state_list = {
1349                 'confirmed': _('is scheduled %s.') % msg,
1350                 'assigned': _('is ready to process.'),
1351                 'cancel': _('is cancelled.'),
1352                 'done': _('is done.'),
1353                 'auto': _('is waiting.'),
1354                 'draft': _('is in draft state.'),
1355             }
1356             res = data_obj.get_object_reference(cr, uid, 'stock', view_list.get(pick.type, 'view_picking_form'))
1357             context.update({'view_id': res and res[1] or False})
1358             message += state_list[pick.state]
1359             self.log(cr, uid, pick.id, message, context=context)
1360         return True
1361
1362 stock_picking()
1363
1364 class stock_production_lot(osv.osv):
1365
1366     def name_get(self, cr, uid, ids, context=None):
1367         if not ids:
1368             return []
1369         reads = self.read(cr, uid, ids, ['name', 'prefix', 'ref'], context)
1370         res = []
1371         for record in reads:
1372             name = record['name']
1373             prefix = record['prefix']
1374             if prefix:
1375                 name = prefix + '/' + name
1376             if record['ref']:
1377                 name = '%s [%s]' % (name, record['ref'])
1378             res.append((record['id'], name))
1379         return res
1380     
1381     def name_search(self, cr, uid, name, args=None, operator='ilike', context=None, limit=100):
1382         args = args or []
1383         ids = []
1384         if name:
1385             ids = self.search(cr, uid, [('prefix', '=', name)] + args, limit=limit, context=context)
1386             if not ids:
1387                 ids = self.search(cr, uid, [('name', operator, name)] + args, limit=limit, context=context)
1388         else:
1389             ids = self.search(cr, uid, args, limit=limit, context=context)
1390         return self.name_get(cr, uid, ids, context)
1391
1392     _name = 'stock.production.lot'
1393     _description = 'Production lot'
1394
1395     def _get_stock(self, cr, uid, ids, field_name, arg, context=None):
1396         """ Gets stock of products for locations
1397         @return: Dictionary of values
1398         """
1399         if context is None:
1400             context = {}
1401         if 'location_id' not in context:
1402             locations = self.pool.get('stock.location').search(cr, uid, [('usage', '=', 'internal')], context=context)
1403         else:
1404             locations = context['location_id'] and [context['location_id']] or []
1405
1406         if isinstance(ids, (int, long)):
1407             ids = [ids]
1408
1409         res = {}.fromkeys(ids, 0.0)
1410         if locations:
1411             cr.execute('''select
1412                     prodlot_id,
1413                     sum(qty)
1414                 from
1415                     stock_report_prodlots
1416                 where
1417                     location_id IN %s and prodlot_id IN %s group by prodlot_id''',(tuple(locations),tuple(ids),))
1418             res.update(dict(cr.fetchall()))
1419
1420         return res
1421
1422     def _stock_search(self, cr, uid, obj, name, args, context=None):
1423         """ Searches Ids of products
1424         @return: Ids of locations
1425         """
1426         locations = self.pool.get('stock.location').search(cr, uid, [('usage', '=', 'internal')])
1427         cr.execute('''select
1428                 prodlot_id,
1429                 sum(qty)
1430             from
1431                 stock_report_prodlots
1432             where
1433                 location_id IN %s group by prodlot_id
1434             having  sum(qty) '''+ str(args[0][1]) + str(args[0][2]),(tuple(locations),))
1435         res = cr.fetchall()
1436         ids = [('id', 'in', map(lambda x: x[0], res))]
1437         return ids
1438
1439     _columns = {
1440         'name': fields.char('Production Lot', size=64, required=True, help="Unique production lot, will be displayed as: PREFIX/SERIAL [INT_REF]"),
1441         'ref': fields.char('Internal Reference', size=256, help="Internal reference number in case it differs from the manufacturer's serial number"),
1442         'prefix': fields.char('Prefix', size=64, help="Optional prefix to prepend when displaying this serial number: PREFIX/SERIAL [INT_REF]"),
1443         'product_id': fields.many2one('product.product', 'Product', required=True, domain=[('type', '<>', 'service')]),
1444         'date': fields.datetime('Creation Date', required=True),
1445         'stock_available': fields.function(_get_stock, fnct_search=_stock_search, type="float", string="Available", select=True,
1446             help="Current quantity of products with this Production Lot Number available in company warehouses",
1447             digits_compute=dp.get_precision('Product UoM')),
1448         'revisions': fields.one2many('stock.production.lot.revision', 'lot_id', 'Revisions'),
1449         'company_id': fields.many2one('res.company', 'Company', select=True),
1450         'move_ids': fields.one2many('stock.move', 'prodlot_id', 'Moves for this production lot', readonly=True),
1451     }
1452     _defaults = {
1453         'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
1454         'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'stock.lot.serial'),
1455         'product_id': lambda x, y, z, c: c.get('product_id', False),
1456     }
1457     _sql_constraints = [
1458         ('name_ref_uniq', 'unique (name, ref)', 'The combination of serial number and internal reference must be unique !'),
1459     ]
1460     def action_traceability(self, cr, uid, ids, context=None):
1461         """ It traces the information of a product
1462         @param self: The object pointer.
1463         @param cr: A database cursor
1464         @param uid: ID of the user currently logged in
1465         @param ids: List of IDs selected
1466         @param context: A standard dictionary
1467         @return: A dictionary of values
1468         """
1469         value=self.pool.get('action.traceability').action_traceability(cr,uid,ids,context)
1470         return value
1471 stock_production_lot()
1472
1473 class stock_production_lot_revision(osv.osv):
1474     _name = 'stock.production.lot.revision'
1475     _description = 'Production lot revisions'
1476
1477     _columns = {
1478         'name': fields.char('Revision Name', size=64, required=True),
1479         'description': fields.text('Description'),
1480         'date': fields.date('Revision Date'),
1481         'indice': fields.char('Revision Number', size=16),
1482         'author_id': fields.many2one('res.users', 'Author'),
1483         'lot_id': fields.many2one('stock.production.lot', 'Production lot', select=True, ondelete='cascade'),
1484         'company_id': fields.related('lot_id','company_id',type='many2one',relation='res.company',string='Company', store=True, readonly=True),
1485     }
1486
1487     _defaults = {
1488         'author_id': lambda x, y, z, c: z,
1489         'date': time.strftime('%Y-%m-%d'),
1490     }
1491
1492 stock_production_lot_revision()
1493
1494 # ----------------------------------------------------
1495 # Move
1496 # ----------------------------------------------------
1497
1498 #
1499 # Fields:
1500 #   location_dest_id is only used for predicting futur stocks
1501 #
1502 class stock_move(osv.osv):
1503
1504     def _getSSCC(self, cr, uid, context=None):
1505         cr.execute('select id from stock_tracking where create_uid=%s order by id desc limit 1', (uid,))
1506         res = cr.fetchone()
1507         return (res and res[0]) or False
1508     _name = "stock.move"
1509     _description = "Stock Move"
1510     _order = 'date_expected desc, id'
1511     _log_create = False
1512
1513     def action_partial_move(self, cr, uid, ids, context=None):
1514         if context is None: context = {}
1515         if context.get('active_model') != self._name:
1516             context.update(active_ids=ids, active_model=self._name)
1517         partial_id = self.pool.get("stock.partial.move").create(
1518             cr, uid, {}, context=context)
1519         return {
1520             'name':_("Products to Process"),
1521             'view_mode': 'form',
1522             'view_id': False,
1523             'view_type': 'form',
1524             'res_model': 'stock.partial.move',
1525             'res_id': partial_id,
1526             'type': 'ir.actions.act_window',
1527             'nodestroy': True,
1528             'target': 'new',
1529             'domain': '[]',
1530             'context': context
1531         }
1532
1533
1534     def name_get(self, cr, uid, ids, context=None):
1535         res = []
1536         for line in self.browse(cr, uid, ids, context=context):
1537             res.append((line.id, (line.product_id.code or '/')+': '+line.location_id.name+' > '+line.location_dest_id.name))
1538         return res
1539
1540     def _check_tracking(self, cr, uid, ids, context=None):
1541         """ Checks if production lot is assigned to stock move or not.
1542         @return: True or False
1543         """
1544         for move in self.browse(cr, uid, ids, context=context):
1545             if not move.prodlot_id and \
1546                (move.state == 'done' and \
1547                ( \
1548                    (move.product_id.track_production and move.location_id.usage == 'production') or \
1549                    (move.product_id.track_production and move.location_dest_id.usage == 'production') or \
1550                    (move.product_id.track_incoming and move.location_id.usage == 'supplier') or \
1551                    (move.product_id.track_outgoing and move.location_dest_id.usage == 'customer') \
1552                )):
1553                 return False
1554         return True
1555
1556     def _check_product_lot(self, cr, uid, ids, context=None):
1557         """ Checks whether move is done or not and production lot is assigned to that move.
1558         @return: True or False
1559         """
1560         for move in self.browse(cr, uid, ids, context=context):
1561             if move.prodlot_id and move.state == 'done' and (move.prodlot_id.product_id.id != move.product_id.id):
1562                 return False
1563         return True
1564
1565     _columns = {
1566         'name': fields.char('Name', size=250, required=True, select=True),
1567         'priority': fields.selection([('0', 'Not urgent'), ('1', 'Urgent')], 'Priority'),
1568         'create_date': fields.datetime('Creation Date', readonly=True, select=True),
1569         '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)]}),
1570         'date_expected': fields.datetime('Scheduled Date', states={'done': [('readonly', True)]},required=True, select=True, help="Scheduled date for the processing of this move"),
1571         'product_id': fields.many2one('product.product', 'Product', required=True, select=True, domain=[('type','<>','service')],states={'done': [('readonly', True)]}),
1572
1573         'product_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product UoM'), required=True,states={'done': [('readonly', True)]}),
1574         'product_uom': fields.many2one('product.uom', 'Unit of Measure', required=True,states={'done': [('readonly', True)]}),
1575         'product_uos_qty': fields.float('Quantity (UOS)', digits_compute=dp.get_precision('Product UoM'), states={'done': [('readonly', True)]}),
1576         'product_uos': fields.many2one('product.uom', 'Product UOS', states={'done': [('readonly', True)]}),
1577         'product_packaging': fields.many2one('product.packaging', 'Packaging', help="It specifies attributes of packaging like type, quantity of packaging,etc."),
1578
1579         '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."),
1580         '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."),
1581         'address_id': fields.many2one('res.partner.address', 'Destination Address ', states={'done': [('readonly', True)]}, help="Optional address where goods are to be delivered, specifically used for allotment"),
1582
1583         'prodlot_id': fields.many2one('stock.production.lot', 'Production Lot', states={'done': [('readonly', True)]}, help="Production lot is used to put a serial number on the production", select=True),
1584         'tracking_id': fields.many2one('stock.tracking', 'Pack', select=True, states={'done': [('readonly', True)]}, help="Logistical shipping unit: pallet, box, pack ..."),
1585
1586         'auto_validate': fields.boolean('Auto Validate'),
1587
1588         'move_dest_id': fields.many2one('stock.move', 'Destination Move', help="Optional: next stock move when chaining them", select=True),
1589         'move_history_ids': fields.many2many('stock.move', 'stock_move_history_ids', 'parent_id', 'child_id', 'Move History (child moves)'),
1590         'move_history_ids2': fields.many2many('stock.move', 'stock_move_history_ids', 'child_id', 'parent_id', 'Move History (parent moves)'),
1591         'picking_id': fields.many2one('stock.picking', 'Reference', select=True,states={'done': [('readonly', True)]}),
1592         'note': fields.text('Notes'),
1593         'state': fields.selection([('draft', 'New'), ('waiting', 'Waiting Another Move'), ('confirmed', 'Waiting Availability'), ('assigned', 'Available'), ('done', 'Done'), ('cancel', 'Cancelled')], 'State', readonly=True, select=True,
1594               help='When the stock move is created it is in the \'Draft\' state.\n After that, it is set to \'Not Available\' state if the scheduler did not find the products.\n When products are reserved it is set to \'Available\'.\n When the picking is done the state is \'Done\'.\
1595               \nThe state is \'Waiting\' if the move is waiting for another one.'),
1596         'price_unit': fields.float('Unit Price', digits_compute= dp.get_precision('Account'), help="Technical field used to record the product cost set by the user during a picking confirmation (when average price costing method is used)"),
1597         'price_currency_id': fields.many2one('res.currency', 'Currency for average price', help="Technical field used to record the currency chosen by the user during a picking confirmation (when average price costing method is used)"),
1598         'company_id': fields.many2one('res.company', 'Company', required=True, select=True),
1599         'partner_id': fields.related('picking_id','address_id','partner_id',type='many2one', relation="res.partner", string="Partner", store=True, select=True),
1600         'backorder_id': fields.related('picking_id','backorder_id',type='many2one', relation="stock.picking", string="Back Order", select=True),
1601         'origin': fields.related('picking_id','origin',type='char', size=64, relation="stock.picking", string="Origin", store=True),
1602
1603         # used for colors in tree views:
1604         'scrapped': fields.related('location_dest_id','scrap_location',type='boolean',relation='stock.location',string='Scrapped', readonly=True),
1605     }
1606     def _check_location(self, cr, uid, ids, context=None):
1607         for record in self.browse(cr, uid, ids, context=context):
1608             if (record.state=='done') and (record.location_dest_id.usage == 'view' or record.location_id.usage == 'view'):
1609                 return False
1610         return True
1611
1612     _constraints = [
1613         (_check_tracking,
1614             'You must assign a production lot for this product',
1615             ['prodlot_id']),
1616         (_check_location, 'You can not move products from or to a location of the type view.',
1617             ['location_id','location_dest_id']),
1618         (_check_product_lot,
1619             'You try to assign a lot which is not from the same product',
1620             ['prodlot_id'])]
1621
1622     def _default_location_destination(self, cr, uid, context=None):
1623         """ Gets default address of partner for destination location
1624         @return: Address id or False
1625         """
1626         mod_obj = self.pool.get('ir.model.data')
1627         picking_type = context.get('picking_type')
1628         location_id = False
1629
1630         if context is None:
1631             context = {}
1632         if context.get('move_line', []):
1633             if context['move_line'][0]:
1634                 if isinstance(context['move_line'][0], (tuple, list)):
1635                     location_id = context['move_line'][0][2] and context['move_line'][0][2].get('location_dest_id',False)
1636                 else:
1637                     move_list = self.pool.get('stock.move').read(cr, uid, context['move_line'][0], ['location_dest_id'])
1638                     location_id = move_list and move_list['location_dest_id'][0] or False
1639         elif context.get('address_out_id', False):
1640             property_out = self.pool.get('res.partner.address').browse(cr, uid, context['address_out_id'], context).partner_id.property_stock_customer
1641             location_id = property_out and property_out.id or False
1642         else:
1643             location_xml_id = False
1644             if picking_type == 'in':
1645                 location_xml_id = 'stock_location_stock'
1646             elif picking_type == 'out':
1647                 location_xml_id = 'stock_location_customers'
1648             if location_xml_id:
1649                 location_model, location_id = mod_obj.get_object_reference(cr, uid, 'stock', location_xml_id)
1650         return location_id
1651
1652     def _default_location_source(self, cr, uid, context=None):
1653         """ Gets default address of partner for source location
1654         @return: Address id or False
1655         """
1656         mod_obj = self.pool.get('ir.model.data')
1657         picking_type = context.get('picking_type')
1658         location_id = False
1659
1660         if context is None:
1661             context = {}
1662         if context.get('move_line', []):
1663             try:
1664                 location_id = context['move_line'][0][2]['location_id']
1665             except:
1666                 pass
1667         elif context.get('address_in_id', False):
1668             part_obj_add = self.pool.get('res.partner.address').browse(cr, uid, context['address_in_id'], context=context)
1669             if part_obj_add.partner_id:
1670                 location_id = part_obj_add.partner_id.property_stock_supplier.id
1671         else:
1672             location_xml_id = False
1673             if picking_type == 'in':
1674                 location_xml_id = 'stock_location_suppliers'
1675             elif picking_type == 'out':
1676                 location_xml_id = 'stock_location_stock'
1677             if location_xml_id:
1678                 location_model, location_id = mod_obj.get_object_reference(cr, uid, 'stock', location_xml_id)
1679         return location_id
1680
1681     _defaults = {
1682         'location_id': _default_location_source,
1683         'location_dest_id': _default_location_destination,
1684         'state': 'draft',
1685         'priority': '1',
1686         'product_qty': 1.0,
1687         'scrapped' :  False,
1688         'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
1689         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.move', context=c),
1690         'date_expected': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
1691     }
1692
1693     def write(self, cr, uid, ids, vals, context=None):
1694         if isinstance(ids, (int, long)):
1695             ids = [ids]
1696         if uid != 1:
1697             frozen_fields = set(['product_qty', 'product_uom', 'product_uos_qty', 'product_uos', 'location_id', 'location_dest_id', 'product_id'])
1698             for move in self.browse(cr, uid, ids, context=context):
1699                 if move.state == 'done':
1700                     if frozen_fields.intersection(vals):
1701                         raise osv.except_osv(_('Operation forbidden'),
1702                                              _('Quantities, UoMs, Products and Locations cannot be modified on stock moves that have already been processed (except by the Administrator)'))
1703         return  super(stock_move, self).write(cr, uid, ids, vals, context=context)
1704
1705     def copy(self, cr, uid, id, default=None, context=None):
1706         if default is None:
1707             default = {}
1708         default = default.copy()
1709         default.update({'move_history_ids2': [], 'move_history_ids': []})
1710         return super(stock_move, self).copy(cr, uid, id, default, context=context)
1711
1712     def _auto_init(self, cursor, context=None):
1713         res = super(stock_move, self)._auto_init(cursor, context=context)
1714         cursor.execute('SELECT indexname \
1715                 FROM pg_indexes \
1716                 WHERE indexname = \'stock_move_location_id_location_dest_id_product_id_state\'')
1717         if not cursor.fetchone():
1718             cursor.execute('CREATE INDEX stock_move_location_id_location_dest_id_product_id_state \
1719                     ON stock_move (location_id, location_dest_id, product_id, state)')
1720         return res
1721
1722     def onchange_lot_id(self, cr, uid, ids, prodlot_id=False, product_qty=False,
1723                         loc_id=False, product_id=False, uom_id=False, context=None):
1724         """ On change of production lot gives a warning message.
1725         @param prodlot_id: Changed production lot id
1726         @param product_qty: Quantity of product
1727         @param loc_id: Location id
1728         @param product_id: Product id
1729         @return: Warning message
1730         """
1731         if not prodlot_id or not loc_id:
1732             return {}
1733         ctx = context and context.copy() or {}
1734         ctx['location_id'] = loc_id
1735         ctx.update({'raise-exception': True})
1736         uom_obj = self.pool.get('product.uom')
1737         product_obj = self.pool.get('product.product')
1738         product_uom = product_obj.browse(cr, uid, product_id, context=ctx).uom_id
1739         prodlot = self.pool.get('stock.production.lot').browse(cr, uid, prodlot_id, context=ctx)
1740         location = self.pool.get('stock.location').browse(cr, uid, loc_id, context=ctx)
1741         uom = uom_obj.browse(cr, uid, uom_id, context=ctx)
1742         amount_actual = uom_obj._compute_qty_obj(cr, uid, product_uom, prodlot.stock_available, uom, context=ctx)
1743         warning = {}
1744         if (location.usage == 'internal') and (product_qty > (amount_actual or 0.0)):
1745             warning = {
1746                 'title': _('Insufficient Stock in Lot !'),
1747                 'message': _('You are moving %.2f %s products but only %.2f %s available in this lot.') % (product_qty, uom.name, amount_actual, uom.name)
1748             }
1749         return {'warning': warning}
1750
1751     def onchange_quantity(self, cr, uid, ids, product_id, product_qty,
1752                           product_uom, product_uos):
1753         """ On change of product quantity finds UoM and UoS quantities
1754         @param product_id: Product id
1755         @param product_qty: Changed Quantity of product
1756         @param product_uom: Unit of measure of product
1757         @param product_uos: Unit of sale of product
1758         @return: Dictionary of values
1759         """
1760         result = {
1761                   'product_uos_qty': 0.00
1762           }
1763
1764         if (not product_id) or (product_qty <=0.0):
1765             return {'value': result}
1766
1767         product_obj = self.pool.get('product.product')
1768         uos_coeff = product_obj.read(cr, uid, product_id, ['uos_coeff'])
1769
1770         if product_uos and product_uom and (product_uom != product_uos):
1771             result['product_uos_qty'] = product_qty * uos_coeff['uos_coeff']
1772         else:
1773             result['product_uos_qty'] = product_qty
1774
1775         return {'value': result}
1776
1777     def onchange_uos_quantity(self, cr, uid, ids, product_id, product_uos_qty,
1778                           product_uos, product_uom):
1779         """ On change of product quantity finds UoM and UoS quantities
1780         @param product_id: Product id
1781         @param product_uos_qty: Changed UoS Quantity of product
1782         @param product_uom: Unit of measure of product
1783         @param product_uos: Unit of sale of product
1784         @return: Dictionary of values
1785         """
1786         result = {
1787                   'product_qty': 0.00
1788           }
1789
1790         if (not product_id) or (product_uos_qty <=0.0):
1791             return {'value': result}
1792
1793         product_obj = self.pool.get('product.product')
1794         uos_coeff = product_obj.read(cr, uid, product_id, ['uos_coeff'])
1795
1796         if product_uos and product_uom and (product_uom != product_uos):
1797             result['product_qty'] = product_uos_qty / uos_coeff['uos_coeff']
1798         else:
1799             result['product_qty'] = product_uos_qty
1800
1801         return {'value': result}
1802
1803     def onchange_product_id(self, cr, uid, ids, prod_id=False, loc_id=False,
1804                             loc_dest_id=False, address_id=False):
1805         """ On change of product id, if finds UoM, UoS, quantity and UoS quantity.
1806         @param prod_id: Changed Product id
1807         @param loc_id: Source location id
1808         @param loc_dest_id: Destination location id
1809         @param address_id: Address id of partner
1810         @return: Dictionary of values
1811         """
1812         if not prod_id:
1813             return {}
1814         lang = False
1815         if address_id:
1816             addr_rec = self.pool.get('res.partner.address').browse(cr, uid, address_id)
1817             if addr_rec:
1818                 lang = addr_rec.partner_id and addr_rec.partner_id.lang or False
1819         ctx = {'lang': lang}
1820
1821         product = self.pool.get('product.product').browse(cr, uid, [prod_id], context=ctx)[0]
1822         uos_id  = product.uos_id and product.uos_id.id or False
1823         result = {
1824             'product_uom': product.uom_id.id,
1825             'product_uos': uos_id,
1826             'product_qty': 1.00,
1827             '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']
1828         }
1829         if not ids:
1830             result['name'] = product.partner_ref
1831         if loc_id:
1832             result['location_id'] = loc_id
1833         if loc_dest_id:
1834             result['location_dest_id'] = loc_dest_id
1835         return {'value': result}
1836
1837     def onchange_date(self, cr, uid, ids, date, date_expected, context=None):
1838         """ On change of Scheduled Date gives a Move date.
1839         @param date_expected: Scheduled Date
1840         @param date: Move Date
1841         @return: Move Date
1842         """
1843         if not date_expected:
1844             date_expected = time.strftime('%Y-%m-%d %H:%M:%S')
1845         return {'value':{'date': date_expected}}
1846
1847     def _chain_compute(self, cr, uid, moves, context=None):
1848         """ Finds whether the location has chained location type or not.
1849         @param moves: Stock moves
1850         @return: Dictionary containing destination location with chained location type.
1851         """
1852         result = {}
1853         for m in moves:
1854             dest = self.pool.get('stock.location').chained_location_get(
1855                 cr,
1856                 uid,
1857                 m.location_dest_id,
1858                 m.picking_id and m.picking_id.address_id and m.picking_id.address_id.partner_id,
1859                 m.product_id,
1860                 context
1861             )
1862             if dest:
1863                 if dest[1] == 'transparent':
1864                     newdate = (datetime.strptime(m.date, '%Y-%m-%d %H:%M:%S') + relativedelta(days=dest[2] or 0)).strftime('%Y-%m-%d')
1865                     self.write(cr, uid, [m.id], {
1866                         'date': newdate,
1867                         'location_dest_id': dest[0].id})
1868                     if m.picking_id and (dest[3] or dest[5]):
1869                         self.pool.get('stock.picking').write(cr, uid, [m.picking_id.id], {
1870                             'stock_journal_id': dest[3] or m.picking_id.stock_journal_id.id,
1871                             'type': dest[5] or m.picking_id.type
1872                         }, context=context)
1873                     m.location_dest_id = dest[0]
1874                     res2 = self._chain_compute(cr, uid, [m], context=context)
1875                     for pick_id in res2.keys():
1876                         result.setdefault(pick_id, [])
1877                         result[pick_id] += res2[pick_id]
1878                 else:
1879                     result.setdefault(m.picking_id, [])
1880                     result[m.picking_id].append( (m, dest) )
1881         return result
1882
1883     def _prepare_chained_picking(self, cr, uid, picking_name, picking, picking_type, moves_todo, context=None):
1884         """Prepare the definition (values) to create a new chained picking.
1885
1886            :param str picking_name: desired new picking name
1887            :param browse_record picking: source picking (being chained to)
1888            :param str picking_type: desired new picking type
1889            :param list moves_todo: specification of the stock moves to be later included in this
1890                picking, in the form::
1891
1892                    [[move, (dest_location, auto_packing, chained_delay, chained_journal,
1893                                   chained_company_id, chained_picking_type)],
1894                     ...
1895                    ]
1896
1897                See also :meth:`stock_location.chained_location_get`.
1898         """
1899         res_company = self.pool.get('res.company')
1900         return {
1901                     'name': picking_name,
1902                     'origin': tools.ustr(picking.origin or ''),
1903                     'type': picking_type,
1904                     'note': picking.note,
1905                     'move_type': picking.move_type,
1906                     'auto_picking': moves_todo[0][1][1] == 'auto',
1907                     'stock_journal_id': moves_todo[0][1][3],
1908                     'company_id': moves_todo[0][1][4] or res_company._company_default_get(cr, uid, 'stock.company', context=context),
1909                     'address_id': picking.address_id.id,
1910                     'invoice_state': 'none',
1911                     'date': picking.date,
1912                 }
1913
1914     def _create_chained_picking(self, cr, uid, picking_name, picking, picking_type, moves_todo, context=None):
1915         picking_obj = self.pool.get('stock.picking')
1916         return picking_obj.create(cr, uid, self._prepare_chained_picking(cr, uid, picking_name, picking, picking_type, moves_todo, context=context))
1917
1918     def create_chained_picking(self, cr, uid, moves, context=None):
1919         res_obj = self.pool.get('res.company')
1920         location_obj = self.pool.get('stock.location')
1921         move_obj = self.pool.get('stock.move')
1922         wf_service = netsvc.LocalService("workflow")
1923         new_moves = []
1924         if context is None:
1925             context = {}
1926         seq_obj = self.pool.get('ir.sequence')
1927         for picking, todo in self._chain_compute(cr, uid, moves, context=context).items():
1928             ptype = todo[0][1][5] and todo[0][1][5] or location_obj.picking_type_get(cr, uid, todo[0][0].location_dest_id, todo[0][1][0])
1929             if picking:
1930                 # name of new picking according to its type
1931                 new_pick_name = seq_obj.get(cr, uid, 'stock.picking.' + ptype)
1932                 pickid = self._create_chained_picking(cr, uid, new_pick_name, picking, ptype, todo, context=context)
1933                 # Need to check name of old picking because it always considers picking as "OUT" when created from Sale Order
1934                 old_ptype = location_obj.picking_type_get(cr, uid, picking.move_lines[0].location_id, picking.move_lines[0].location_dest_id)
1935                 if old_ptype != picking.type:
1936                     old_pick_name = seq_obj.get(cr, uid, 'stock.picking.' + old_ptype)
1937                     self.pool.get('stock.picking').write(cr, uid, [picking.id], {'name': old_pick_name}, context=context)
1938             else:
1939                 pickid = False
1940             for move, (loc, dummy, delay, dummy, company_id, ptype) in todo:
1941                 new_id = move_obj.copy(cr, uid, move.id, {
1942                     'location_id': move.location_dest_id.id,
1943                     'location_dest_id': loc.id,
1944                     'date_moved': time.strftime('%Y-%m-%d'),
1945                     'picking_id': pickid,
1946                     'state': 'waiting',
1947                     'company_id': company_id or res_obj._company_default_get(cr, uid, 'stock.company', context=context)  ,
1948                     'move_history_ids': [],
1949                     'date': (datetime.strptime(move.date, '%Y-%m-%d %H:%M:%S') + relativedelta(days=delay or 0)).strftime('%Y-%m-%d'),
1950                     'move_history_ids2': []}
1951                 )
1952                 move_obj.write(cr, uid, [move.id], {
1953                     'move_dest_id': new_id,
1954                     'move_history_ids': [(4, new_id)]
1955                 })
1956                 new_moves.append(self.browse(cr, uid, [new_id])[0])
1957             if pickid:
1958                 wf_service.trg_validate(uid, 'stock.picking', pickid, 'button_confirm', cr)
1959         if new_moves:
1960             new_moves += self.create_chained_picking(cr, uid, new_moves, context)
1961         return new_moves
1962
1963     def action_confirm(self, cr, uid, ids, context=None):
1964         """ Confirms stock move.
1965         @return: List of ids.
1966         """
1967         moves = self.browse(cr, uid, ids, context=context)
1968         self.write(cr, uid, ids, {'state': 'confirmed'})
1969         self.create_chained_picking(cr, uid, moves, context)
1970         return []
1971
1972     def action_assign(self, cr, uid, ids, *args):
1973         """ Changes state to confirmed or waiting.
1974         @return: List of values
1975         """
1976         todo = []
1977         for move in self.browse(cr, uid, ids):
1978             if move.state in ('confirmed', 'waiting'):
1979                 todo.append(move.id)
1980         res = self.check_assign(cr, uid, todo)
1981         return res
1982
1983     def force_assign(self, cr, uid, ids, context=None):
1984         """ Changes the state to assigned.
1985         @return: True
1986         """
1987         self.write(cr, uid, ids, {'state': 'assigned'})
1988         return True
1989
1990     def cancel_assign(self, cr, uid, ids, context=None):
1991         """ Changes the state to confirmed.
1992         @return: True
1993         """
1994         self.write(cr, uid, ids, {'state': 'confirmed'})
1995
1996         # fix for bug lp:707031
1997         # called write of related picking because changing move availability does
1998         # not trigger workflow of picking in order to change the state of picking
1999         wf_service = netsvc.LocalService('workflow')
2000         for move in self.browse(cr, uid, ids, context):
2001             if move.picking_id:
2002                 wf_service.trg_write(uid, 'stock.picking', move.picking_id.id, cr)
2003         return True
2004
2005     #
2006     # Duplicate stock.move
2007     #
2008     def check_assign(self, cr, uid, ids, context=None):
2009         """ Checks the product type and accordingly writes the state.
2010         @return: No. of moves done
2011         """
2012         done = []
2013         count = 0
2014         pickings = {}
2015         if context is None:
2016             context = {}
2017         for move in self.browse(cr, uid, ids, context=context):
2018             if move.product_id.type == 'consu' or move.location_id.usage == 'supplier':
2019                 if move.state in ('confirmed', 'waiting'):
2020                     done.append(move.id)
2021                 pickings[move.picking_id.id] = 1
2022                 continue
2023             if move.state in ('confirmed', 'waiting'):
2024                 # Important: we must pass lock=True to _product_reserve() to avoid race conditions and double reservations
2025                 res = self.pool.get('stock.location')._product_reserve(cr, uid, [move.location_id.id], move.product_id.id, move.product_qty, {'uom': move.product_uom.id}, lock=True)
2026                 if res:
2027                     #_product_available_test depends on the next status for correct functioning
2028                     #the test does not work correctly if the same product occurs multiple times
2029                     #in the same order. This is e.g. the case when using the button 'split in two' of
2030                     #the stock outgoing form
2031                     self.write(cr, uid, [move.id], {'state':'assigned'})
2032                     done.append(move.id)
2033                     pickings[move.picking_id.id] = 1
2034                     r = res.pop(0)
2035                     cr.execute('update stock_move set location_id=%s, product_qty=%s where id=%s', (r[1], r[0], move.id))
2036
2037                     while res:
2038                         r = res.pop(0)
2039                         move_id = self.copy(cr, uid, move.id, {'product_qty': r[0], 'location_id': r[1]})
2040                         done.append(move_id)
2041         if done:
2042             count += len(done)
2043             self.write(cr, uid, done, {'state': 'assigned'})
2044
2045         if count:
2046             for pick_id in pickings:
2047                 wf_service = netsvc.LocalService("workflow")
2048                 wf_service.trg_write(uid, 'stock.picking', pick_id, cr)
2049         return count
2050
2051     def setlast_tracking(self, cr, uid, ids, context=None):
2052         tracking_obj = self.pool.get('stock.tracking')
2053         picking = self.browse(cr, uid, ids, context=context)[0].picking_id
2054         if picking:
2055             last_track = [line.tracking_id.id for line in picking.move_lines if line.tracking_id]
2056             if not last_track:
2057                 last_track = tracking_obj.create(cr, uid, {}, context=context)
2058             else:
2059                 last_track.sort()
2060                 last_track = last_track[-1]
2061             self.write(cr, uid, ids, {'tracking_id': last_track})
2062         return True
2063
2064     #
2065     # Cancel move => cancel others move and pickings
2066     #
2067     def action_cancel(self, cr, uid, ids, context=None):
2068         """ Cancels the moves and if all moves are cancelled it cancels the picking.
2069         @return: True
2070         """
2071         if not len(ids):
2072             return True
2073         if context is None:
2074             context = {}
2075         pickings = {}
2076         for move in self.browse(cr, uid, ids, context=context):
2077             if move.state in ('confirmed', 'waiting', 'assigned', 'draft'):
2078                 if move.picking_id:
2079                     pickings[move.picking_id.id] = True
2080             if move.move_dest_id and move.move_dest_id.state == 'waiting':
2081                 self.write(cr, uid, [move.move_dest_id.id], {'state': 'assigned'})
2082                 if context.get('call_unlink',False) and move.move_dest_id.picking_id:
2083                     wf_service = netsvc.LocalService("workflow")
2084                     wf_service.trg_write(uid, 'stock.picking', move.move_dest_id.picking_id.id, cr)
2085         self.write(cr, uid, ids, {'state': 'cancel', 'move_dest_id': False})
2086         if not context.get('call_unlink',False):
2087             for pick in self.pool.get('stock.picking').browse(cr, uid, pickings.keys()):
2088                 if all(move.state == 'cancel' for move in pick.move_lines):
2089                     self.pool.get('stock.picking').write(cr, uid, [pick.id], {'state': 'cancel'})
2090
2091         wf_service = netsvc.LocalService("workflow")
2092         for id in ids:
2093             wf_service.trg_trigger(uid, 'stock.move', id, cr)
2094         return True
2095
2096     def _get_accounting_data_for_valuation(self, cr, uid, move, context=None):
2097         """
2098         Return the accounts and journal to use to post Journal Entries for the real-time
2099         valuation of the move.
2100
2101         :param context: context dictionary that can explicitly mention the company to consider via the 'force_company' key
2102         :raise: osv.except_osv() is any mandatory account or journal is not defined.
2103         """
2104         product_obj=self.pool.get('product.product')
2105         accounts = product_obj.get_product_accounts(cr, uid, move.product_id.id, context)
2106         if move.location_id.valuation_out_account_id:
2107             acc_src = move.location_id.valuation_out_account_id.id
2108         else:
2109             acc_src = accounts['stock_account_input']
2110
2111         if move.location_dest_id.valuation_in_account_id:
2112             acc_dest = move.location_dest_id.valuation_in_account_id.id
2113         else:
2114             acc_dest = accounts['stock_account_output']
2115
2116         acc_valuation = accounts.get('property_stock_valuation_account_id', False)
2117         journal_id = accounts['stock_journal']
2118
2119         if acc_dest == acc_valuation:
2120             raise osv.except_osv(_('Error!'),  _('Can not create Journal Entry, Output Account defined on this product and Valuation account on category of this product are same.'))
2121
2122         if acc_src == acc_valuation:
2123             raise osv.except_osv(_('Error!'),  _('Can not create Journal Entry, Input Account defined on this product and Valuation account on category of this product are same.'))
2124
2125         if not acc_src:
2126             raise osv.except_osv(_('Error!'),  _('There is no stock input account defined for this product or its category: "%s" (id: %d)') % \
2127                                     (move.product_id.name, move.product_id.id,))
2128         if not acc_dest:
2129             raise osv.except_osv(_('Error!'),  _('There is no stock output account defined for this product or its category: "%s" (id: %d)') % \
2130                                     (move.product_id.name, move.product_id.id,))
2131         if not journal_id:
2132             raise osv.except_osv(_('Error!'), _('There is no journal defined on the product category: "%s" (id: %d)') % \
2133                                     (move.product_id.categ_id.name, move.product_id.categ_id.id,))
2134         if not acc_valuation:
2135             raise osv.except_osv(_('Error!'), _('There is no inventory Valuation account defined on the product category: "%s" (id: %d)') % \
2136                                     (move.product_id.categ_id.name, move.product_id.categ_id.id,))
2137         return journal_id, acc_src, acc_dest, acc_valuation
2138
2139     def _get_reference_accounting_values_for_valuation(self, cr, uid, move, context=None):
2140         """
2141         Return the reference amount and reference currency representing the inventory valuation for this move.
2142         These reference values should possibly be converted before being posted in Journals to adapt to the primary
2143         and secondary currencies of the relevant accounts.
2144         """
2145         product_uom_obj = self.pool.get('product.uom')
2146
2147         # by default the reference currency is that of the move's company
2148         reference_currency_id = move.company_id.currency_id.id
2149
2150         default_uom = move.product_id.uom_id.id
2151         qty = product_uom_obj._compute_qty(cr, uid, move.product_uom.id, move.product_qty, default_uom)
2152
2153         # if product is set to average price and a specific value was entered in the picking wizard,
2154         # we use it
2155         if move.product_id.cost_method == 'average' and move.price_unit:
2156             reference_amount = qty * move.price_unit
2157             reference_currency_id = move.price_currency_id.id or reference_currency_id
2158
2159         # Otherwise we default to the company's valuation price type, considering that the values of the
2160         # valuation field are expressed in the default currency of the move's company.
2161         else:
2162             if context is None:
2163                 context = {}
2164             currency_ctx = dict(context, currency_id = move.company_id.currency_id.id)
2165             amount_unit = move.product_id.price_get('standard_price', context=currency_ctx)[move.product_id.id]
2166             reference_amount = amount_unit * qty or 1.0
2167
2168         return reference_amount, reference_currency_id
2169
2170
2171     def _create_product_valuation_moves(self, cr, uid, move, context=None):
2172         """
2173         Generate the appropriate accounting moves if the product being moves is subject
2174         to real_time valuation tracking, and the source or destination location is
2175         a transit location or is outside of the company.
2176         """
2177         if move.product_id.valuation == 'real_time': # FIXME: product valuation should perhaps be a property?
2178             if context is None:
2179                 context = {}
2180             src_company_ctx = dict(context,force_company=move.location_id.company_id.id)
2181             dest_company_ctx = dict(context,force_company=move.location_dest_id.company_id.id)
2182             account_moves = []
2183             # Outgoing moves (or cross-company output part)
2184             if move.location_id.company_id \
2185                 and (move.location_id.usage == 'internal' and move.location_dest_id.usage != 'internal'\
2186                      or move.location_id.company_id != move.location_dest_id.company_id):
2187                 journal_id, acc_src, acc_dest, acc_valuation = self._get_accounting_data_for_valuation(cr, uid, move, src_company_ctx)
2188                 reference_amount, reference_currency_id = self._get_reference_accounting_values_for_valuation(cr, uid, move, src_company_ctx)
2189                 account_moves += [(journal_id, self._create_account_move_line(cr, uid, move, acc_valuation, acc_dest, reference_amount, reference_currency_id, context))]
2190
2191             # Incoming moves (or cross-company input part)
2192             if move.location_dest_id.company_id \
2193                 and (move.location_id.usage != 'internal' and move.location_dest_id.usage == 'internal'\
2194                      or move.location_id.company_id != move.location_dest_id.company_id):
2195                 journal_id, acc_src, acc_dest, acc_valuation = self._get_accounting_data_for_valuation(cr, uid, move, dest_company_ctx)
2196                 reference_amount, reference_currency_id = self._get_reference_accounting_values_for_valuation(cr, uid, move, src_company_ctx)
2197                 account_moves += [(journal_id, self._create_account_move_line(cr, uid, move, acc_src, acc_valuation, reference_amount, reference_currency_id, context))]
2198
2199             move_obj = self.pool.get('account.move')
2200             for j_id, move_lines in account_moves:
2201                 move_obj.create(cr, uid,
2202                         {
2203                          'journal_id': j_id,
2204                          'line_id': move_lines,
2205                          'ref': move.picking_id and move.picking_id.name})
2206
2207
2208     def action_done(self, cr, uid, ids, context=None):
2209         """ Makes the move done and if all moves are done, it will finish the picking.
2210         @return:
2211         """
2212         picking_ids = []
2213         move_ids = []
2214         wf_service = netsvc.LocalService("workflow")
2215         if context is None:
2216             context = {}
2217
2218         todo = []
2219         for move in self.browse(cr, uid, ids, context=context):
2220             if move.state=="draft":
2221                 todo.append(move.id)
2222         if todo:
2223             self.action_confirm(cr, uid, todo, context=context)
2224             todo = []
2225
2226         for move in self.browse(cr, uid, ids, context=context):
2227             if move.state in ['done','cancel']:
2228                 continue
2229             move_ids.append(move.id)
2230
2231             if move.picking_id:
2232                 picking_ids.append(move.picking_id.id)
2233             if move.move_dest_id.id and (move.state != 'done'):
2234                 self.write(cr, uid, [move.id], {'move_history_ids': [(4, move.move_dest_id.id)]})
2235                 #cr.execute('insert into stock_move_history_ids (parent_id,child_id) values (%s,%s)', (move.id, move.move_dest_id.id))
2236                 if move.move_dest_id.state in ('waiting', 'confirmed'):
2237                     self.force_assign(cr, uid, [move.move_dest_id.id], context=context)
2238                     if move.move_dest_id.picking_id:
2239                         wf_service.trg_write(uid, 'stock.picking', move.move_dest_id.picking_id.id, cr)
2240                     if move.move_dest_id.auto_validate:
2241                         self.action_done(cr, uid, [move.move_dest_id.id], context=context)
2242
2243             self._create_product_valuation_moves(cr, uid, move, context=context)
2244             if move.state not in ('confirmed','done','assigned'):
2245                 todo.append(move.id)
2246
2247         if todo:
2248             self.action_confirm(cr, uid, todo, context=context)
2249
2250         self.write(cr, uid, move_ids, {'state': 'done', 'date': time.strftime('%Y-%m-%d %H:%M:%S')}, context=context)
2251         for id in move_ids:
2252              wf_service.trg_trigger(uid, 'stock.move', id, cr)
2253
2254         for pick_id in picking_ids:
2255             wf_service.trg_write(uid, 'stock.picking', pick_id, cr)
2256
2257         return True
2258
2259     def _create_account_move_line(self, cr, uid, move, src_account_id, dest_account_id, reference_amount, reference_currency_id, context=None):
2260         """
2261         Generate the account.move.line values to post to track the stock valuation difference due to the
2262         processing of the given stock move.
2263         """
2264         # prepare default values considering that the destination accounts have the reference_currency_id as their main currency
2265         partner_id = (move.picking_id.address_id and move.picking_id.address_id.partner_id and move.picking_id.address_id.partner_id.id) or False
2266         debit_line_vals = {
2267                     'name': move.name,
2268                     'product_id': move.product_id and move.product_id.id or False,
2269                     'quantity': move.product_qty,
2270                     'ref': move.picking_id and move.picking_id.name or False,
2271                     'date': time.strftime('%Y-%m-%d'),
2272                     'partner_id': partner_id,
2273                     'debit': reference_amount,
2274                     'account_id': dest_account_id,
2275         }
2276         credit_line_vals = {
2277                     'name': move.name,
2278                     'product_id': move.product_id and move.product_id.id or False,
2279                     'quantity': move.product_qty,
2280                     'ref': move.picking_id and move.picking_id.name or False,
2281                     'date': time.strftime('%Y-%m-%d'),
2282                     'partner_id': partner_id,
2283                     'credit': reference_amount,
2284                     'account_id': src_account_id,
2285         }
2286
2287         # if we are posting to accounts in a different currency, provide correct values in both currencies correctly
2288         # when compatible with the optional secondary currency on the account.
2289         # Financial Accounts only accept amounts in secondary currencies if there's no secondary currency on the account
2290         # or if it's the same as that of the secondary amount being posted.
2291         account_obj = self.pool.get('account.account')
2292         src_acct, dest_acct = account_obj.browse(cr, uid, [src_account_id, dest_account_id], context=context)
2293         src_main_currency_id = src_acct.company_id.currency_id.id
2294         dest_main_currency_id = dest_acct.company_id.currency_id.id
2295         cur_obj = self.pool.get('res.currency')
2296         if reference_currency_id != src_main_currency_id:
2297             # fix credit line:
2298             credit_line_vals['credit'] = cur_obj.compute(cr, uid, reference_currency_id, src_main_currency_id, reference_amount, context=context)
2299             if (not src_acct.currency_id) or src_acct.currency_id.id == reference_currency_id:
2300                 credit_line_vals.update(currency_id=reference_currency_id, amount_currency=reference_amount)
2301         if reference_currency_id != dest_main_currency_id:
2302             # fix debit line:
2303             debit_line_vals['debit'] = cur_obj.compute(cr, uid, reference_currency_id, dest_main_currency_id, reference_amount, context=context)
2304             if (not dest_acct.currency_id) or dest_acct.currency_id.id == reference_currency_id:
2305                 debit_line_vals.update(currency_id=reference_currency_id, amount_currency=reference_amount)
2306
2307         return [(0, 0, debit_line_vals), (0, 0, credit_line_vals)]
2308
2309     def unlink(self, cr, uid, ids, context=None):
2310         if context is None:
2311             context = {}
2312         ctx = context.copy()
2313         for move in self.browse(cr, uid, ids, context=context):
2314             if move.state != 'draft' and not ctx.get('call_unlink',False):
2315                 raise osv.except_osv(_('UserError'),
2316                         _('You can only delete draft moves.'))
2317         return super(stock_move, self).unlink(
2318             cr, uid, ids, context=ctx)
2319
2320     # _create_lot function is not used anywhere
2321     def _create_lot(self, cr, uid, ids, product_id, prefix=False):
2322         """ Creates production lot
2323         @return: Production lot id
2324         """
2325         prodlot_obj = self.pool.get('stock.production.lot')
2326         prodlot_id = prodlot_obj.create(cr, uid, {'prefix': prefix, 'product_id': product_id})
2327         return prodlot_id
2328
2329     def action_scrap(self, cr, uid, ids, quantity, location_id, context=None):
2330         """ Move the scrap/damaged product into scrap location
2331         @param cr: the database cursor
2332         @param uid: the user id
2333         @param ids: ids of stock move object to be scrapped
2334         @param quantity : specify scrap qty
2335         @param location_id : specify scrap location
2336         @param context: context arguments
2337         @return: Scraped lines
2338         """
2339         #quantity should in MOVE UOM
2340         if quantity <= 0:
2341             raise osv.except_osv(_('Warning!'), _('Please provide a positive quantity to scrap!'))
2342         res = []
2343         for move in self.browse(cr, uid, ids, context=context):
2344             move_qty = move.product_qty
2345             uos_qty = quantity / move_qty * move.product_uos_qty
2346             default_val = {
2347                 'product_qty': quantity,
2348                 'product_uos_qty': uos_qty,
2349                 'state': move.state,
2350                 'scrapped' : True,
2351                 'location_dest_id': location_id,
2352                 'tracking_id': move.tracking_id.id,
2353                 'prodlot_id': move.prodlot_id.id,
2354             }
2355             if move.location_id.usage <> 'internal':
2356                 default_val.update({'location_id': move.location_dest_id.id})
2357             new_move = self.copy(cr, uid, move.id, default_val)
2358
2359             res += [new_move]
2360             product_obj = self.pool.get('product.product')
2361             for (id, name) in product_obj.name_get(cr, uid, [move.product_id.id]):
2362                 self.log(cr, uid, move.id, "%s x %s %s" % (quantity, name, _("were scrapped")))
2363
2364         self.action_done(cr, uid, res, context=context)
2365         return res
2366
2367     # action_split function is not used anywhere
2368     def action_split(self, cr, uid, ids, quantity, split_by_qty=1, prefix=False, with_lot=True, context=None):
2369         """ Split Stock Move lines into production lot which specified split by quantity.
2370         @param cr: the database cursor
2371         @param uid: the user id
2372         @param ids: ids of stock move object to be splited
2373         @param split_by_qty : specify split by qty
2374         @param prefix : specify prefix of production lot
2375         @param with_lot : if true, prodcution lot will assign for split line otherwise not.
2376         @param context: context arguments
2377         @return: Splited move lines
2378         """
2379
2380         if context is None:
2381             context = {}
2382         if quantity <= 0:
2383             raise osv.except_osv(_('Warning!'), _('Please provide Proper Quantity !'))
2384
2385         res = []
2386
2387         for move in self.browse(cr, uid, ids, context=context):
2388             if split_by_qty <= 0 or quantity == 0:
2389                 return res
2390
2391             uos_qty = split_by_qty / move.product_qty * move.product_uos_qty
2392
2393             quantity_rest = quantity % split_by_qty
2394             uos_qty_rest = split_by_qty / move.product_qty * move.product_uos_qty
2395
2396             update_val = {
2397                 'product_qty': split_by_qty,
2398                 'product_uos_qty': uos_qty,
2399             }
2400             for idx in range(int(quantity//split_by_qty)):
2401                 if not idx and move.product_qty<=quantity:
2402                     current_move = move.id
2403                 else:
2404                     current_move = self.copy(cr, uid, move.id, {'state': move.state})
2405                 res.append(current_move)
2406                 if with_lot:
2407                     update_val['prodlot_id'] = self._create_lot(cr, uid, [current_move], move.product_id.id)
2408
2409                 self.write(cr, uid, [current_move], update_val)
2410
2411
2412             if quantity_rest > 0:
2413                 idx = int(quantity//split_by_qty)
2414                 update_val['product_qty'] = quantity_rest
2415                 update_val['product_uos_qty'] = uos_qty_rest
2416                 if not idx and move.product_qty<=quantity:
2417                     current_move = move.id
2418                 else:
2419                     current_move = self.copy(cr, uid, move.id, {'state': move.state})
2420
2421                 res.append(current_move)
2422
2423
2424                 if with_lot:
2425                     update_val['prodlot_id'] = self._create_lot(cr, uid, [current_move], move.product_id.id)
2426
2427                 self.write(cr, uid, [current_move], update_val)
2428         return res
2429
2430     def action_consume(self, cr, uid, ids, quantity, location_id=False, context=None):
2431         """ Consumed product with specific quatity from specific source location
2432         @param cr: the database cursor
2433         @param uid: the user id
2434         @param ids: ids of stock move object to be consumed
2435         @param quantity : specify consume quantity
2436         @param location_id : specify source location
2437         @param context: context arguments
2438         @return: Consumed lines
2439         """
2440         #quantity should in MOVE UOM
2441         if context is None:
2442             context = {}
2443         if quantity <= 0:
2444             raise osv.except_osv(_('Warning!'), _('Please provide Proper Quantity !'))
2445         res = []
2446         for move in self.browse(cr, uid, ids, context=context):
2447             move_qty = move.product_qty
2448             if move_qty <= 0:
2449                 raise osv.except_osv(_('Error!'), _('Can not consume a move with negative or zero quantity !'))
2450             quantity_rest = move.product_qty
2451             quantity_rest -= quantity
2452             uos_qty_rest = quantity_rest / move_qty * move.product_uos_qty
2453             if quantity_rest <= 0:
2454                 quantity_rest = 0
2455                 uos_qty_rest = 0
2456                 quantity = move.product_qty
2457
2458             uos_qty = quantity / move_qty * move.product_uos_qty
2459             location_dest_id = move.product_id.property_stock_production or move.location_dest_id
2460             if quantity_rest > 0:
2461                 default_val = {
2462                     'product_qty': quantity,
2463                     'product_uos_qty': uos_qty,
2464                     'state': move.state,
2465                     'location_id': location_id or move.location_id.id,
2466                     'location_dest_id': location_dest_id.id,
2467                 }
2468                 current_move = self.copy(cr, uid, move.id, default_val)
2469                 res += [current_move]
2470                 update_val = {}
2471                 update_val['product_qty'] = quantity_rest
2472                 update_val['product_uos_qty'] = uos_qty_rest
2473                 self.write(cr, uid, [move.id], update_val)
2474
2475             else:
2476                 quantity_rest = quantity
2477                 uos_qty_rest =  uos_qty
2478                 res += [move.id]
2479                 update_val = {
2480                         'product_qty' : quantity_rest,
2481                         'product_uos_qty' : uos_qty_rest,
2482                         'location_id': location_id or move.location_id.id,
2483                         'location_dest_id': location_dest_id.id,
2484                 }
2485                 self.write(cr, uid, [move.id], update_val)
2486
2487             product_obj = self.pool.get('product.product')
2488             for new_move in self.browse(cr, uid, res, context=context):
2489                 for (id, name) in product_obj.name_get(cr, uid, [new_move.product_id.id]):
2490                     message = _("Product  '%s' is consumed with '%s' quantity.") %(name, new_move.product_qty)
2491                     self.log(cr, uid, new_move.id, message)
2492         self.action_done(cr, uid, res, context=context)
2493
2494         return res
2495
2496     # FIXME: needs refactoring, this code is partially duplicated in stock_picking.do_partial()!
2497     def do_partial(self, cr, uid, ids, partial_datas, context=None):
2498         """ Makes partial pickings and moves done.
2499         @param partial_datas: Dictionary containing details of partial picking
2500                           like partner_id, address_id, delivery_date, delivery
2501                           moves with product_id, product_qty, uom
2502         """
2503         res = {}
2504         picking_obj = self.pool.get('stock.picking')
2505         product_obj = self.pool.get('product.product')
2506         currency_obj = self.pool.get('res.currency')
2507         uom_obj = self.pool.get('product.uom')
2508         wf_service = netsvc.LocalService("workflow")
2509
2510         if context is None:
2511             context = {}
2512
2513         complete, too_many, too_few = [], [], []
2514         move_product_qty = {}
2515         prodlot_ids = {}
2516         for move in self.browse(cr, uid, ids, context=context):
2517             if move.state in ('done', 'cancel'):
2518                 continue
2519             partial_data = partial_datas.get('move%s'%(move.id), False)
2520             assert partial_data, _('Missing partial picking data for move #%s') % (move.id)
2521             product_qty = partial_data.get('product_qty',0.0)
2522             move_product_qty[move.id] = product_qty
2523             product_uom = partial_data.get('product_uom',False)
2524             product_price = partial_data.get('product_price',0.0)
2525             product_currency = partial_data.get('product_currency',False)
2526             prodlot_ids[move.id] = partial_data.get('prodlot_id')
2527             if move.product_qty == product_qty:
2528                 complete.append(move)
2529             elif move.product_qty > product_qty:
2530                 too_few.append(move)
2531             else:
2532                 too_many.append(move)
2533
2534             # Average price computation
2535             if (move.picking_id.type == 'in') and (move.product_id.cost_method == 'average'):
2536                 product = product_obj.browse(cr, uid, move.product_id.id)
2537                 move_currency_id = move.company_id.currency_id.id
2538                 context['currency_id'] = move_currency_id
2539                 qty = uom_obj._compute_qty(cr, uid, product_uom, product_qty, product.uom_id.id)
2540                 if qty > 0:
2541                     new_price = currency_obj.compute(cr, uid, product_currency,
2542                             move_currency_id, product_price)
2543                     new_price = uom_obj._compute_price(cr, uid, product_uom, new_price,
2544                             product.uom_id.id)
2545                     if product.qty_available <= 0:
2546                         new_std_price = new_price
2547                     else:
2548                         # Get the standard price
2549                         amount_unit = product.price_get('standard_price', context=context)[product.id]
2550                         new_std_price = ((amount_unit * product.qty_available)\
2551                             + (new_price * qty))/(product.qty_available + qty)
2552
2553                     product_obj.write(cr, uid, [product.id],{'standard_price': new_std_price})
2554
2555                     # Record the values that were chosen in the wizard, so they can be
2556                     # used for inventory valuation if real-time valuation is enabled.
2557                     self.write(cr, uid, [move.id],
2558                                 {'price_unit': product_price,
2559                                  'price_currency_id': product_currency,
2560                                 })
2561
2562         for move in too_few:
2563             product_qty = move_product_qty[move.id]
2564             if product_qty != 0:
2565                 defaults = {
2566                             'product_qty' : product_qty,
2567                             'product_uos_qty': product_qty,
2568                             'picking_id' : move.picking_id.id,
2569                             'state': 'assigned',
2570                             'move_dest_id': False,
2571                             'price_unit': move.price_unit,
2572                             }
2573                 prodlot_id = prodlot_ids[move.id]
2574                 if prodlot_id:
2575                     defaults.update(prodlot_id=prodlot_id)
2576                 new_move = self.copy(cr, uid, move.id, defaults)
2577                 complete.append(self.browse(cr, uid, new_move))
2578             self.write(cr, uid, [move.id],
2579                     {
2580                         'product_qty' : move.product_qty - product_qty,
2581                         'product_uos_qty':move.product_qty - product_qty,
2582                     })
2583
2584
2585         for move in too_many:
2586             self.write(cr, uid, [move.id],
2587                     {
2588                         'product_qty': move.product_qty,
2589                         'product_uos_qty': move.product_qty,
2590                     })
2591             complete.append(move)
2592
2593         for move in complete:
2594             if prodlot_ids.get(move.id):
2595                 self.write(cr, uid, [move.id],{'prodlot_id': prodlot_ids.get(move.id)})
2596             self.action_done(cr, uid, [move.id], context=context)
2597             if  move.picking_id.id :
2598                 # TOCHECK : Done picking if all moves are done
2599                 cr.execute("""
2600                     SELECT move.id FROM stock_picking pick
2601                     RIGHT JOIN stock_move move ON move.picking_id = pick.id AND move.state = %s
2602                     WHERE pick.id = %s""",
2603                             ('done', move.picking_id.id))
2604                 res = cr.fetchall()
2605                 if len(res) == len(move.picking_id.move_lines):
2606                     picking_obj.action_move(cr, uid, [move.picking_id.id])
2607                     wf_service.trg_validate(uid, 'stock.picking', move.picking_id.id, 'button_done', cr)
2608
2609         return [move.id for move in complete]
2610
2611 stock_move()
2612
2613 class stock_inventory(osv.osv):
2614     _name = "stock.inventory"
2615     _description = "Inventory"
2616     _columns = {
2617         'name': fields.char('Inventory Reference', size=64, required=True, readonly=True, states={'draft': [('readonly', False)]}),
2618         'date': fields.datetime('Creation Date', required=True, readonly=True, states={'draft': [('readonly', False)]}),
2619         'date_done': fields.datetime('Date done'),
2620         'inventory_line_id': fields.one2many('stock.inventory.line', 'inventory_id', 'Inventories', states={'done': [('readonly', True)]}),
2621         'move_ids': fields.many2many('stock.move', 'stock_inventory_move_rel', 'inventory_id', 'move_id', 'Created Moves'),
2622         'state': fields.selection( (('draft', 'Draft'), ('done', 'Done'), ('confirm','Confirmed'),('cancel','Cancelled')), 'State', readonly=True, select=True),
2623         'company_id': fields.many2one('res.company', 'Company', required=True, select=True, readonly=True, states={'draft':[('readonly',False)]}),
2624
2625     }
2626     _defaults = {
2627         'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
2628         'state': 'draft',
2629         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.inventory', context=c)
2630     }
2631
2632     def _inventory_line_hook(self, cr, uid, inventory_line, move_vals):
2633         """ Creates a stock move from an inventory line
2634         @param inventory_line:
2635         @param move_vals:
2636         @return:
2637         """
2638         return self.pool.get('stock.move').create(cr, uid, move_vals)
2639
2640     def action_done(self, cr, uid, ids, context=None):
2641         """ Finish the inventory
2642         @return: True
2643         """
2644         if context is None:
2645             context = {}
2646         move_obj = self.pool.get('stock.move')
2647         for inv in self.browse(cr, uid, ids, context=context):
2648             move_obj.action_done(cr, uid, [x.id for x in inv.move_ids], context=context)
2649             self.write(cr, uid, [inv.id], {'state':'done', 'date_done': time.strftime('%Y-%m-%d %H:%M:%S')}, context=context)
2650         return True
2651
2652     def action_confirm(self, cr, uid, ids, context=None):
2653         """ Confirm the inventory and writes its finished date
2654         @return: True
2655         """
2656         if context is None:
2657             context = {}
2658         # to perform the correct inventory corrections we need analyze stock location by
2659         # location, never recursively, so we use a special context
2660         product_context = dict(context, compute_child=False)
2661
2662         location_obj = self.pool.get('stock.location')
2663         for inv in self.browse(cr, uid, ids, context=context):
2664             move_ids = []
2665             for line in inv.inventory_line_id:
2666                 pid = line.product_id.id
2667                 product_context.update(uom=line.product_uom.id, date=inv.date, prodlot_id=line.prod_lot_id.id)
2668                 amount = location_obj._product_get(cr, uid, line.location_id.id, [pid], product_context)[pid]
2669
2670                 change = line.product_qty - amount
2671                 lot_id = line.prod_lot_id.id
2672                 if change:
2673                     location_id = line.product_id.product_tmpl_id.property_stock_inventory.id
2674                     value = {
2675                         'name': 'INV:' + str(line.inventory_id.id) + ':' + line.inventory_id.name,
2676                         'product_id': line.product_id.id,
2677                         'product_uom': line.product_uom.id,
2678                         'prodlot_id': lot_id,
2679                         'date': inv.date,
2680                     }
2681                     if change > 0:
2682                         value.update( {
2683                             'product_qty': change,
2684                             'location_id': location_id,
2685                             'location_dest_id': line.location_id.id,
2686                         })
2687                     else:
2688                         value.update( {
2689                             'product_qty': -change,
2690                             'location_id': line.location_id.id,
2691                             'location_dest_id': location_id,
2692                         })
2693                     move_ids.append(self._inventory_line_hook(cr, uid, line, value))
2694             message = _("Inventory '%s' is done.") %(inv.name)
2695             self.log(cr, uid, inv.id, message)
2696             self.write(cr, uid, [inv.id], {'state': 'confirm', 'move_ids': [(6, 0, move_ids)]})
2697             self.pool.get('stock.move').action_confirm(cr, uid, move_ids, context=context)
2698         return True
2699
2700     def action_cancel_draft(self, cr, uid, ids, context=None):
2701         """ Cancels the stock move and change inventory state to draft.
2702         @return: True
2703         """
2704         for inv in self.browse(cr, uid, ids, context=context):
2705             self.pool.get('stock.move').action_cancel(cr, uid, [x.id for x in inv.move_ids], context=context)
2706             self.write(cr, uid, [inv.id], {'state':'draft'}, context=context)
2707         return True
2708
2709     def action_cancel_inventory(self, cr, uid, ids, context=None):
2710         """ Cancels both stock move and inventory
2711         @return: True
2712         """
2713         move_obj = self.pool.get('stock.move')
2714         account_move_obj = self.pool.get('account.move')
2715         for inv in self.browse(cr, uid, ids, context=context):
2716             move_obj.action_cancel(cr, uid, [x.id for x in inv.move_ids], context=context)
2717             for move in inv.move_ids:
2718                  account_move_ids = account_move_obj.search(cr, uid, [('name', '=', move.name)])
2719                  if account_move_ids:
2720                      account_move_data_l = account_move_obj.read(cr, uid, account_move_ids, ['state'], context=context)
2721                      for account_move in account_move_data_l:
2722                          if account_move['state'] == 'posted':
2723                              raise osv.except_osv(_('UserError'),
2724                                                   _('In order to cancel this inventory, you must first unpost related journal entries.'))
2725                          account_move_obj.unlink(cr, uid, [account_move['id']], context=context)
2726             self.write(cr, uid, [inv.id], {'state': 'cancel'}, context=context)
2727         return True
2728
2729 stock_inventory()
2730
2731 class stock_inventory_line(osv.osv):
2732     _name = "stock.inventory.line"
2733     _description = "Inventory Line"
2734     _rec_name = "inventory_id"
2735     _columns = {
2736         'inventory_id': fields.many2one('stock.inventory', 'Inventory', ondelete='cascade', select=True),
2737         'location_id': fields.many2one('stock.location', 'Location', required=True),
2738         'product_id': fields.many2one('product.product', 'Product', required=True, select=True),
2739         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
2740         'product_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product UoM')),
2741         'company_id': fields.related('inventory_id','company_id',type='many2one',relation='res.company',string='Company',store=True, select=True, readonly=True),
2742         'prod_lot_id': fields.many2one('stock.production.lot', 'Production Lot', domain="[('product_id','=',product_id)]"),
2743         'state': fields.related('inventory_id','state',type='char',string='State',readonly=True),
2744     }
2745
2746     def on_change_product_id(self, cr, uid, ids, location_id, product, uom=False, to_date=False):
2747         """ Changes UoM and name if product_id changes.
2748         @param location_id: Location id
2749         @param product: Changed product_id
2750         @param uom: UoM product
2751         @return:  Dictionary of changed values
2752         """
2753         if not product:
2754             return {'value': {'product_qty': 0.0, 'product_uom': False}}
2755         obj_product = self.pool.get('product.product').browse(cr, uid, product)
2756         uom = uom or obj_product.uom_id.id
2757         amount = self.pool.get('stock.location')._product_get(cr, uid, location_id, [product], {'uom': uom, 'to_date': to_date, 'compute_child': False})[product]
2758         result = {'product_qty': amount, 'product_uom': uom}
2759         return {'value': result}
2760
2761 stock_inventory_line()
2762
2763 #----------------------------------------------------------
2764 # Stock Warehouse
2765 #----------------------------------------------------------
2766 class stock_warehouse(osv.osv):
2767     _name = "stock.warehouse"
2768     _description = "Warehouse"
2769     _columns = {
2770         'name': fields.char('Name', size=128, required=True, select=True),
2771         'company_id': fields.many2one('res.company', 'Company', required=True, select=True),
2772         'partner_address_id': fields.many2one('res.partner.address', 'Owner Address'),
2773         'lot_input_id': fields.many2one('stock.location', 'Location Input', required=True, domain=[('usage','<>','view')]),
2774         'lot_stock_id': fields.many2one('stock.location', 'Location Stock', required=True, domain=[('usage','=','internal')]),
2775         'lot_output_id': fields.many2one('stock.location', 'Location Output', required=True, domain=[('usage','<>','view')]),
2776     }
2777     _defaults = {
2778         'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.inventory', context=c),
2779     }
2780
2781 stock_warehouse()
2782
2783 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: