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