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