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