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