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