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