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