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