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