[IMP] Stock: make the latest inventory date report usefull by displaying the product...
[odoo/odoo.git] / addons / stock / stock.py
1 # -*- encoding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
6 #    $Id$
7 #
8 #    This program is free software: you can redistribute it and/or modify
9 #    it under the terms of the GNU General Public License as published by
10 #    the Free Software Foundation, either version 3 of the License, or
11 #    (at your option) any later version.
12 #
13 #    This program is distributed in the hope that it will be useful,
14 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
15 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 #    GNU General Public License for more details.
17 #
18 #    You should have received a copy of the GNU General Public License
19 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
20 #
21 ##############################################################################
22
23 from mx import DateTime
24 import time
25 import netsvc
26 from osv import fields, osv
27 from tools import config
28 from tools.translate import _
29 import tools
30 import logging
31
32
33 #----------------------------------------------------------
34 # Incoterms
35 #----------------------------------------------------------
36 class stock_incoterms(osv.osv):
37     _name = "stock.incoterms"
38     _description = "Incoterms"
39     _columns = {
40         'name': fields.char('Name', size=64, required=True),
41         'code': fields.char('Code', size=3, required=True),
42         'active': fields.boolean('Active'),
43     }
44     _defaults = {
45         'active': lambda *a: True,
46     }
47
48 stock_incoterms()
49
50
51 #----------------------------------------------------------
52 # Stock Location
53 #----------------------------------------------------------
54 class stock_location(osv.osv):
55     _name = "stock.location"
56     _description = "Location"
57     _parent_name = "location_id"
58     _parent_store = True
59     _parent_order = 'id'
60     _order = 'parent_left'
61
62     def _complete_name(self, cr, uid, ids, name, args, context):
63         def _get_one_full_name(location, level=4):
64             if location.location_id:
65                 parent_path = _get_one_full_name(location.location_id, level-1) + "/"
66             else:
67                 parent_path = ''
68             return parent_path + location.name
69         res = {}
70         for m in self.browse(cr, uid, ids, context=context):
71             res[m.id] = _get_one_full_name(m)
72         return res
73
74     def _product_qty_available(self, cr, uid, ids, field_names, arg, context={}):
75         res = {}
76         for id in ids:
77             res[id] = {}.fromkeys(field_names, 0.0)
78         if ('product_id' not in context) or not ids:
79             return res
80         #location_ids = self.search(cr, uid, [('location_id', 'child_of', ids)])
81         for loc in ids:
82             context['location'] = [loc]
83             prod = self.pool.get('product.product').browse(cr, uid, context['product_id'], context)
84             if 'stock_real' in field_names:
85                 res[loc]['stock_real'] = prod.qty_available
86             if 'stock_virtual' in field_names:
87                 res[loc]['stock_virtual'] = prod.virtual_available
88         return res
89
90     def product_detail(self, cr, uid, id, field, context={}):
91         res = {}
92         res[id] = {}
93         final_value = 0.0
94         field_to_read = 'virtual_available'
95         if field == 'stock_real_value':
96             field_to_read = 'qty_available'
97         cr.execute('select distinct product_id from stock_move where (location_id=%s) or (location_dest_id=%s)', (id, id))
98         result = cr.dictfetchall()
99         if result:
100             for r in result:
101                 c = (context or {}).copy()
102                 c['location'] = id
103                 product = self.pool.get('product.product').read(cr, uid, r['product_id'], [field_to_read, 'standard_price'], context=c)
104                 final_value += (product[field_to_read] * product['standard_price'])
105         return final_value
106
107     def _product_value(self, cr, uid, ids, field_names, arg, context={}):
108         result = {}
109         for id in ids:
110             result[id] = {}.fromkeys(field_names, 0.0)
111         for field_name in field_names:
112             for loc in ids:
113                 ret_dict = self.product_detail(cr, uid, loc, field=field_name)
114                 result[loc][field_name] = ret_dict
115         return result
116
117     _columns = {
118         'name': fields.char('Location Name', size=64, required=True, translate=True),
119         'active': fields.boolean('Active'),
120         'usage': fields.selection([('supplier', 'Supplier Location'), ('view', 'View'), ('internal', 'Internal Location'), ('customer', 'Customer Location'), ('inventory', 'Inventory'), ('procurement', 'Procurement'), ('production', 'Production')], 'Location Type', required=True),
121         'allocation_method': fields.selection([('fifo', 'FIFO'), ('lifo', 'LIFO'), ('nearest', 'Nearest')], 'Allocation Method', required=True),
122
123         'complete_name': fields.function(_complete_name, method=True, type='char', size=100, string="Location Name"),
124
125         'stock_real': fields.function(_product_qty_available, method=True, type='float', string='Real Stock', multi="stock"),
126         'stock_virtual': fields.function(_product_qty_available, method=True, type='float', string='Virtual Stock', multi="stock"),
127
128         'account_id': fields.many2one('account.account', string='Inventory Account', domain=[('type', '!=', 'view')]),
129         'location_id': fields.many2one('stock.location', 'Parent Location', select=True, ondelete='cascade'),
130         'child_ids': fields.one2many('stock.location', 'location_id', 'Contains'),
131
132         'chained_location_id': fields.many2one('stock.location', 'Chained Location If Fixed'),
133         'chained_location_type': fields.selection([('none', 'None'), ('customer', 'Customer'), ('fixed', 'Fixed Location')],
134             'Chained Location Type', required=True),
135         'chained_auto_packing': fields.selection(
136             [('auto', 'Automatic Move'), ('manual', 'Manual Operation'), ('transparent', 'Automatic No Step Added')],
137             'Automatic Move',
138             required=True,
139             help="This is used only if you selected a chained location type.\n" \
140                 "The 'Automatic Move' value will create a stock move after the current one that will be "\
141                 "validated automatically. With 'Manual Operation', the stock move has to be validated "\
142                 "by a worker. With 'Automatic No Step Added', the location is replaced in the original move."
143             ),
144         'chained_delay': fields.integer('Chained Delay (days)'),
145         'address_id': fields.many2one('res.partner.address', 'Location Address'),
146         'icon': fields.selection(tools.icons, 'Icon', size=64),
147
148         'comment': fields.text('Additional Information'),
149         'posx': fields.integer('Corridor (X)'),
150         'posy': fields.integer('Shelves (Y)'),
151         'posz': fields.integer('Height (Z)'),
152
153         'parent_left': fields.integer('Left Parent', select=1),
154         'parent_right': fields.integer('Right Parent', select=1),
155         'stock_real_value': fields.function(_product_value, method=True, type='float', string='Real Stock Value', multi="stock"),
156         'stock_virtual_value': fields.function(_product_value, method=True, type='float', string='Virtual Stock Value', multi="stock"),
157     }
158     _defaults = {
159         'active': lambda *a: 1,
160         'usage': lambda *a: 'internal',
161         'allocation_method': lambda *a: 'fifo',
162         'chained_location_type': lambda *a: 'none',
163         'chained_auto_packing': lambda *a: 'manual',
164         'posx': lambda *a: 0,
165         'posy': lambda *a: 0,
166         'posz': lambda *a: 0,
167         'icon': lambda *a: False
168     }
169
170     def chained_location_get(self, cr, uid, location, partner=None, product=None, context={}):
171         result = None
172         if location.chained_location_type == 'customer':
173             if partner:
174                 result = partner.property_stock_customer
175         elif location.chained_location_type == 'fixed':
176             result = location.chained_location_id
177         if result:
178             return result, location.chained_auto_packing, location.chained_delay
179         return result
180
181     def picking_type_get(self, cr, uid, from_location, to_location, context={}):
182         result = 'internal'
183         if (from_location.usage=='internal') and (to_location and to_location.usage in ('customer', 'supplier')):
184             result = 'delivery'
185         elif (from_location.usage in ('supplier', 'customer')) and (to_location.usage=='internal'):
186             result = 'in'
187         return result
188
189     def _product_get_all_report(self, cr, uid, ids, product_ids=False,
190             context=None):
191         return self._product_get_report(cr, uid, ids, product_ids, context,
192                 recursive=True)
193
194     def _product_get_report(self, cr, uid, ids, product_ids=False,
195             context=None, recursive=False):
196         if context is None:
197             context = {}
198         product_obj = self.pool.get('product.product')
199         if not product_ids:
200             product_ids = product_obj.search(cr, uid, [])
201
202         products = product_obj.browse(cr, uid, product_ids, context=context)
203         products_by_uom = {}
204         products_by_id = {}
205         for product in products:
206             products_by_uom.setdefault(product.uom_id.id, [])
207             products_by_uom[product.uom_id.id].append(product)
208             products_by_id.setdefault(product.id, [])
209             products_by_id[product.id] = product
210
211         result = {}
212         result['product'] = []
213         for id in ids:
214             quantity_total = 0.0
215             total_price = 0.0
216             for uom_id in products_by_uom.keys():
217                 fnc = self._product_get
218                 if recursive:
219                     fnc = self._product_all_get
220                 ctx = context.copy()
221                 ctx['uom'] = uom_id
222                 qty = fnc(cr, uid, id, [x.id for x in products_by_uom[uom_id]],
223                         context=ctx)
224                 for product_id in qty.keys():
225                     if not qty[product_id]:
226                         continue
227                     product = products_by_id[product_id]
228                     quantity_total += qty[product_id]
229                     price = qty[product_id] * product.standard_price
230                     total_price += price
231                     result['product'].append({
232                         'price': product.standard_price,
233                         'prod_name': product.name,
234                         'code': product.default_code, # used by lot_overview_all report!
235                         'variants': product.variants or '',
236                         'uom': product.uom_id.name,
237                         'prod_qty': qty[product_id],
238                         'price_value': price,
239                     })
240         result['total'] = quantity_total
241         result['total_price'] = total_price
242         return result
243
244     def _product_get_multi_location(self, cr, uid, ids, product_ids=False, context={}, states=['done'], what=('in', 'out')):
245         product_obj = self.pool.get('product.product')
246         context.update({
247             'states': states,
248             'what': what,
249             'location': ids
250         })
251         return product_obj.get_product_available(cr, uid, product_ids, context=context)
252
253     def _product_get(self, cr, uid, id, product_ids=False, context={}, states=['done']):
254         ids = id and [id] or []
255         context.update({'compute_child':False})
256         return self._product_get_multi_location(cr, uid, ids, product_ids, context, states)
257
258     def _product_all_get(self, cr, uid, id, product_ids=False, context={}, states=['done']):
259         # build the list of ids of children of the location given by id
260         ids = id and [id] or []
261 #        location_ids = self.search(cr, uid, [('location_id', 'child_of', ids)])
262         return self._product_get_multi_location(cr, uid, ids, product_ids, context, states)
263
264     def _product_virtual_get(self, cr, uid, id, product_ids=False, context={}, states=['done']):
265         return self._product_all_get(cr, uid, id, product_ids, context, ['confirmed', 'waiting', 'assigned', 'done'])
266
267
268     def _product_reserve(self, cr, uid, ids, product_id, product_qty, context=None, lock=False):
269         """
270         Attempt to find a quantity ``product_qty`` (in the product's default uom or the uom passed in ``context``) of product ``product_id``
271         in locations with id ``ids`` and their child locations. If ``lock`` is True, the stock.move lines
272         of product with id ``product_id`` in the searched location will be write-locked using Postgres's
273         "FOR UPDATE NOWAIT" option until the transaction is committed or rolled back, to prevent reservin 
274         twice the same products.
275         If ``lock`` is True and the lock cannot be obtained (because another transaction has locked some of
276         the same stock.move lines), a log line will be output and False will be returned, as if there was
277         not enough stock.
278
279         :param product_id: Id of product to reserve
280         :param product_qty: Quantity of product to reserve (in the product's default uom or the uom passed in ``context``)
281         :param lock: if True, the stock.move lines of product with id ``product_id`` in all locations (and children locations) with ``ids`` will
282                      be write-locked using postgres's "FOR UPDATE NOWAIT" option until the transaction is committed or rolled back. This is
283                      to prevent reserving twice the same products.
284         :param context: optional context dictionary: it a 'uom' key is present it will be used instead of the default product uom to
285                         compute the ``product_qty`` and in the return value.
286         :return: List of tuples in the form (qty, location_id) with the (partial) quantities that can be taken in each location to
287                  reach the requested product_qty (``qty`` is expressed in the default uom of the product), of False if enough
288                  products could not be found, or the lock could not be obtained (and ``lock`` was True).
289         """
290         result = []
291         amount = 0.0
292         if context is None:
293             context = {}
294         for id in self.search(cr, uid, [('location_id', 'child_of', ids)]):
295             if lock:
296                 try:
297                     # Must lock with a separate select query because FOR UPDATE can't be used with
298                     # aggregation/group by's (when individual rows aren't identifiable).
299                     # We use a SAVEPOINT to be able to rollback this part of the transaction without
300                     # failing the whole transaction in case the LOCK cannot be acquired.
301                     cr.execute("SAVEPOINT stock_location_product_reserve")
302                     cr.execute("""SELECT id FROM stock_move
303                                   WHERE product_id=%s AND
304                                           (
305                                             (location_dest_id=%s AND
306                                              location_id<>%s AND
307                                              state='done')
308                                             OR
309                                             (location_id=%s AND
310                                              location_dest_id<>%s AND
311                                              state in ('done', 'assigned'))
312                                           )
313                                   FOR UPDATE of stock_move NOWAIT""", (product_id, id, id, id, id))
314                 except Exception:
315                     # Here it's likely that the FOR UPDATE NOWAIT failed to get the LOCK,
316                     # so we ROLLBACK to the SAVEPOINT to restore the transaction to its earlier
317                     # state, we return False as if the products were not available, and log it:
318                     cr.execute("ROLLBACK TO stock_location_product_reserve")
319                     logger = logging.getLogger('stock.location')
320                     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)
321                     logger.debug("Trace of the failed product reservation attempt: ", exc_info=True)
322                     return False
323
324             # XXX TODO: rewrite this with one single query, possibly even the quantity conversion
325             cr.execute("""SELECT product_uom, sum(product_qty) AS product_qty
326                           FROM stock_move
327                           WHERE location_dest_id=%s AND
328                                 location_id<>%s AND
329                                 product_id=%s AND
330                                 state='done'
331                           GROUP BY product_uom
332                        """,
333                        (id, id, product_id))
334             results = cr.dictfetchall()
335             cr.execute("""SELECT product_uom,-sum(product_qty) AS product_qty
336                           FROM stock_move
337                           WHERE location_id=%s AND
338                                 location_dest_id<>%s AND
339                                 product_id=%s AND
340                                 state in ('done', 'assigned')
341                           GROUP BY product_uom
342                        """,
343                        (id, id, product_id))
344             results += cr.dictfetchall()
345
346             total = 0.0
347             results2 = 0.0
348             for r in results:
349                 amount = self.pool.get('product.uom')._compute_qty(cr, uid, r['product_uom'], r['product_qty'], context.get('uom', False))
350                 results2 += amount
351                 total += amount
352
353             if total <= 0.0:
354                 continue
355
356             amount = results2
357             if amount > 0:
358                 if amount > min(total, product_qty):
359                     amount = min(product_qty, total)
360                 result.append((amount, id))
361                 product_qty -= amount
362                 total -= amount
363                 if product_qty <= 0.0:
364                     return result
365                 if total <= 0.0:
366                     continue
367         return False
368
369 stock_location()
370
371
372 class stock_tracking(osv.osv):
373     _name = "stock.tracking"
374     _description = "Stock Tracking Lots"
375
376     def checksum(sscc):
377         salt = '31' * 8 + '3'
378         sum = 0
379         for sscc_part, salt_part in zip(sscc, salt):
380             sum += int(sscc_part) * int(salt_part)
381         return (10 - (sum % 10)) % 10
382     checksum = staticmethod(checksum)
383
384     def make_sscc(self, cr, uid, context={}):
385         sequence = self.pool.get('ir.sequence').get(cr, uid, 'stock.lot.tracking')
386         return sequence + str(self.checksum(sequence))
387
388     _columns = {
389         'name': fields.char('Tracking', size=64, required=True),
390         'active': fields.boolean('Active'),
391         'serial': fields.char('Reference', size=64),
392         'move_ids': fields.one2many('stock.move', 'tracking_id', 'Moves Tracked'),
393         'date': fields.datetime('Date Created', required=True),
394     }
395     _defaults = {
396         'active': lambda *a: 1,
397         'name': make_sscc,
398         'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
399     }
400
401     def name_search(self, cr, user, name, args=None, operator='ilike', context=None, limit=80):
402         if not args:
403             args = []
404         if not context:
405             context = {}
406         ids = self.search(cr, user, [('serial', '=', name)]+ args, limit=limit, context=context)
407         ids += self.search(cr, user, [('name', operator, name)]+ args, limit=limit, context=context)
408         return self.name_get(cr, user, ids, context)
409
410     def name_get(self, cr, uid, ids, context={}):
411         if not len(ids):
412             return []
413         res = [(r['id'], r['name']+' ['+(r['serial'] or '')+']') for r in self.read(cr, uid, ids, ['name', 'serial'], context)]
414         return res
415
416     def unlink(self, cr, uid, ids, context=None):
417         raise osv.except_osv(_('Error'), _('You can not remove a lot line !'))
418
419 stock_tracking()
420
421
422 #----------------------------------------------------------
423 # Stock Picking
424 #----------------------------------------------------------
425 class stock_picking(osv.osv):
426     _name = "stock.picking"
427     _description = "Packing List"
428
429     def _set_maximum_date(self, cr, uid, ids, name, value, arg, context):
430         if not value:
431             return False
432         if isinstance(ids, (int, long)):
433             ids = [ids]
434         for pick in self.browse(cr, uid, ids, context):
435             sql_str = """update stock_move set
436                     date_planned=%s
437                 where
438                     picking_id=%s """
439             sqlargs = (value, pick.id)
440
441             if pick.max_date:
442                 sql_str += " and (date_planned=%s or date_planned>%s)"
443                 sqlargs += (pick.max_date, value)
444             cr.execute(sql_str, sqlargs)
445         return True
446
447     def _set_minimum_date(self, cr, uid, ids, name, value, arg, context):
448         if not value:
449             return False
450         if isinstance(ids, (int, long)):
451             ids = [ids]
452         for pick in self.browse(cr, uid, ids, context):
453             sql_str = """update stock_move set
454                     date_planned=%s
455                 where
456                     picking_id=%s """
457             sqlargs = (value, pick.id)
458             if pick.min_date:
459                 sql_str += " and (date_planned=%s or date_planned<%s)"
460                 sqlargs += (pick.min_date, value)
461             cr.execute(sql_str, sqlargs)
462         return True
463
464     def get_min_max_date(self, cr, uid, ids, field_name, arg, context={}):
465         res = {}
466         for id in ids:
467             res[id] = {'min_date': False, 'max_date': False}
468         if not ids:
469             return res
470         cr.execute("""select
471                 picking_id,
472                 min(date_planned),
473                 max(date_planned)
474             from
475                 stock_move
476             where
477                 picking_id in %s
478             group by
479                 picking_id""", (tuple(ids),))
480         for pick, dt1, dt2 in cr.fetchall():
481             res[pick]['min_date'] = dt1
482             res[pick]['max_date'] = dt2
483         return res
484
485     def create(self, cr, user, vals, context=None):
486         if ('name' not in vals) or (vals.get('name')=='/'):
487             vals['name'] = self.pool.get('ir.sequence').get(cr, user, 'stock.picking')
488
489         return super(stock_picking, self).create(cr, user, vals, context)
490
491     _columns = {
492         'name': fields.char('Reference', size=64, select=True),
493         'origin': fields.char('Origin Reference', size=64),
494         'backorder_id': fields.many2one('stock.picking', 'Back Order'),
495         'type': fields.selection([('out', 'Sending Goods'), ('in', 'Getting Goods'), ('internal', 'Internal'), ('delivery', 'Delivery')], 'Shipping Type', required=True, select=True),
496         'active': fields.boolean('Active'),
497         'note': fields.text('Notes'),
498
499         'location_id': fields.many2one('stock.location', 'Location'),
500         'location_dest_id': fields.many2one('stock.location', 'Dest. Location'),
501         'move_type': fields.selection([('direct', 'Direct Delivery'), ('one', 'All at once')], 'Delivery Method', required=True),
502         'state': fields.selection([
503             ('draft', 'Draft'),
504             ('auto', 'Waiting'),
505             ('confirmed', 'Confirmed'),
506             ('assigned', 'Available'),
507             ('done', 'Done'),
508             ('cancel', 'Cancelled'),
509             ], 'Status', readonly=True, select=True),
510         'min_date': fields.function(get_min_max_date, fnct_inv=_set_minimum_date, multi="min_max_date",
511                  method=True, store=True, type='datetime', string='Planned Date', select=1),
512         'date': fields.datetime('Date Order'),
513         'date_done': fields.datetime('Date Done'),
514         'max_date': fields.function(get_min_max_date, fnct_inv=_set_maximum_date, multi="min_max_date",
515                  method=True, store=True, type='datetime', string='Max. Planned Date', select=2),
516         'move_lines': fields.one2many('stock.move', 'picking_id', 'Move lines', states={'cancel': [('readonly', True)]}),
517         'auto_picking': fields.boolean('Auto-Packing'),
518         'address_id': fields.many2one('res.partner.address', 'Partner'),
519         'invoice_state': fields.selection([
520             ("invoiced", "Invoiced"),
521             ("2binvoiced", "To Be Invoiced"),
522             ("none", "Not from Packing")], "Invoice Status",
523             select=True, required=True, readonly=True, states={'draft': [('readonly', False)]}),
524     }
525     _defaults = {
526         'name': lambda self, cr, uid, context: '/',
527         'active': lambda *a: 1,
528         'state': lambda *a: 'draft',
529         'move_type': lambda *a: 'direct',
530         'type': lambda *a: 'in',
531         'invoice_state': lambda *a: 'none',
532         'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
533     }
534
535     def copy(self, cr, uid, id, default=None, context={}):
536         if default is None:
537             default = {}
538         default = default.copy()
539         if not default.get('name',False):
540             default['name'] = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking')
541         return super(stock_picking, self).copy(cr, uid, id, default, context)
542
543     def onchange_partner_in(self, cr, uid, context, partner_id=None):
544         return {}
545
546     def action_explode(self, cr, uid, moves, context={}):
547         return moves
548
549     def action_confirm(self, cr, uid, ids, context={}):
550         self.write(cr, uid, ids, {'state': 'confirmed'})
551         todo = []
552         for picking in self.browse(cr, uid, ids):
553             for r in picking.move_lines:
554                 if r.state == 'draft':
555                     todo.append(r.id)
556         todo = self.action_explode(cr, uid, todo, context)
557         if len(todo):
558             self.pool.get('stock.move').action_confirm(cr, uid, todo, context)
559         return True
560
561     def test_auto_picking(self, cr, uid, ids):
562         # TODO: Check locations to see if in the same location ?
563         return True
564
565     def button_confirm(self, cr, uid, ids, *args):
566         for id in ids:
567             wf_service = netsvc.LocalService("workflow")
568             wf_service.trg_validate(uid, 'stock.picking', id, 'button_confirm', cr)
569         self.force_assign(cr, uid, ids, *args)
570         return True
571
572     def action_assign(self, cr, uid, ids, *args):
573         for pick in self.browse(cr, uid, ids):
574             move_ids = [x.id for x in pick.move_lines if x.state == 'confirmed']
575             self.pool.get('stock.move').action_assign(cr, uid, move_ids)
576         return True
577
578     def force_assign(self, cr, uid, ids, *args):
579         wf_service = netsvc.LocalService("workflow")
580         for pick in self.browse(cr, uid, ids):
581             move_ids = [x.id for x in pick.move_lines if x.state in ['confirmed','waiting']]
582 #            move_ids = [x.id for x in pick.move_lines]
583             self.pool.get('stock.move').force_assign(cr, uid, move_ids)
584             wf_service.trg_write(uid, 'stock.picking', pick.id, cr)
585         return True
586
587     def draft_force_assign(self, cr, uid, ids, *args):
588         wf_service = netsvc.LocalService("workflow")
589         for pick in self.browse(cr, uid, ids):
590             wf_service.trg_validate(uid, 'stock.picking', pick.id,
591                 'button_confirm', cr)
592             #move_ids = [x.id for x in pick.move_lines]
593             #self.pool.get('stock.move').force_assign(cr, uid, move_ids)
594             #wf_service.trg_write(uid, 'stock.picking', pick.id, cr)
595         return True
596
597     def draft_validate(self, cr, uid, ids, *args):
598         wf_service = netsvc.LocalService("workflow")
599         self.draft_force_assign(cr, uid, ids)
600         for pick in self.browse(cr, uid, ids):
601             move_ids = [x.id for x in pick.move_lines]
602             self.pool.get('stock.move').force_assign(cr, uid, move_ids)
603             wf_service.trg_write(uid, 'stock.picking', pick.id, cr)
604
605             self.action_move(cr, uid, [pick.id])
606             wf_service.trg_validate(uid, 'stock.picking', pick.id, 'button_done', cr)
607         return True
608
609     def cancel_assign(self, cr, uid, ids, *args):
610         wf_service = netsvc.LocalService("workflow")
611         for pick in self.browse(cr, uid, ids):
612             move_ids = [x.id for x in pick.move_lines]
613             self.pool.get('stock.move').cancel_assign(cr, uid, move_ids)
614             wf_service.trg_write(uid, 'stock.picking', pick.id, cr)
615         return True
616
617     def action_assign_wkf(self, cr, uid, ids):
618         self.write(cr, uid, ids, {'state': 'assigned'})
619         return True
620
621     def test_finnished(self, cr, uid, ids):
622         move_ids = self.pool.get('stock.move').search(cr, uid, [('picking_id', 'in', ids)])
623         for move in self.pool.get('stock.move').browse(cr, uid, move_ids):
624             if move.state not in ('done', 'cancel'):
625                 if move.product_qty != 0.0:
626                     return False
627                 else:
628                     move.write(cr, uid, [move.id], {'state': 'done'})
629         return True
630
631     def test_assigned(self, cr, uid, ids):
632         ok = True
633         for pick in self.browse(cr, uid, ids):
634             mt = pick.move_type
635             for move in pick.move_lines:
636                 if (move.state in ('confirmed', 'draft')) and (mt=='one'):
637                     return False
638                 if (mt=='direct') and (move.state=='assigned') and (move.product_qty):
639                     return True
640                 ok = ok and (move.state in ('cancel', 'done', 'assigned'))
641         return ok
642
643     def action_cancel(self, cr, uid, ids, context={}):
644         for pick in self.browse(cr, uid, ids):
645             ids2 = [move.id for move in pick.move_lines]
646             self.pool.get('stock.move').action_cancel(cr, uid, ids2, context)
647         self.write(cr, uid, ids, {'state': 'cancel', 'invoice_state': 'none'})
648         return True
649
650     #
651     # TODO: change and create a move if not parents
652     #
653     def action_done(self, cr, uid, ids, context=None):
654         self.write(cr, uid, ids, {'state': 'done', 'date_done': time.strftime('%Y-%m-%d %H:%M:%S')})
655         return True
656
657     def action_move(self, cr, uid, ids, context={}):
658         for pick in self.browse(cr, uid, ids):
659             todo = []
660             for move in pick.move_lines:
661                 if move.state == 'assigned':
662                     todo.append(move.id)
663
664             if len(todo):
665                 self.pool.get('stock.move').action_done(cr, uid, todo,
666                         context=context)
667         return True
668
669     def get_currency_id(self, cursor, user, picking):
670         return False
671
672     def _get_payment_term(self, cursor, user, picking):
673         '''Return {'contact': address, 'invoice': address} for invoice'''
674         partner_obj = self.pool.get('res.partner')
675         partner = picking.address_id.partner_id
676         return partner.property_payment_term and partner.property_payment_term.id or False
677
678     def _get_address_invoice(self, cursor, user, picking):
679         '''Return {'contact': address, 'invoice': address} for invoice'''
680         partner_obj = self.pool.get('res.partner')
681         partner = picking.address_id.partner_id
682
683         return partner_obj.address_get(cursor, user, [partner.id],
684                 ['contact', 'invoice'])
685
686     def _get_comment_invoice(self, cursor, user, picking):
687         '''Return comment string for invoice'''
688         return picking.note or ''
689
690     def _get_price_unit_invoice(self, cursor, user, move_line, type):
691         '''Return the price unit for the move line'''
692         if type in ('in_invoice', 'in_refund'):
693             return move_line.product_id.standard_price
694         else:
695             return move_line.product_id.list_price
696
697     def _get_discount_invoice(self, cursor, user, move_line):
698         '''Return the discount for the move line'''
699         return 0.0
700
701     def _get_taxes_invoice(self, cursor, user, move_line, type):
702         '''Return taxes ids for the move line'''
703         if type in ('in_invoice', 'in_refund'):
704             taxes = move_line.product_id.supplier_taxes_id
705         else:
706             taxes = move_line.product_id.taxes_id
707
708         if move_line.picking_id and move_line.picking_id.address_id and move_line.picking_id.address_id.partner_id:
709             return self.pool.get('account.fiscal.position').map_tax(
710                 cursor,
711                 user,
712                 move_line.picking_id.address_id.partner_id.property_account_position,
713                 taxes
714             )
715         else:
716             return map(lambda x: x.id, taxes)
717
718     def _get_account_analytic_invoice(self, cursor, user, picking, move_line):
719         return False
720
721     def _invoice_line_hook(self, cursor, user, move_line, invoice_line_id):
722         '''Call after the creation of the invoice line'''
723         return
724
725     def _invoice_hook(self, cursor, user, picking, invoice_id):
726         '''Call after the creation of the invoice'''
727         return
728
729     def action_invoice_create(self, cursor, user, ids, journal_id=False,
730             group=False, type='out_invoice', context=None):
731         '''Return ids of created invoices for the pickings'''
732         invoice_obj = self.pool.get('account.invoice')
733         invoice_line_obj = self.pool.get('account.invoice.line')
734         invoices_group = {}
735         res = {}
736
737         for picking in self.browse(cursor, user, ids, context=context):
738             if picking.invoice_state != '2binvoiced':
739                 continue
740             payment_term_id = False
741             partner = picking.address_id and picking.address_id.partner_id
742             if not partner:
743                 raise osv.except_osv(_('Error, no partner !'),
744                     _('Please put a partner on the picking list if you want to generate invoice.'))
745
746             if type in ('out_invoice', 'out_refund'):
747                 account_id = partner.property_account_receivable.id
748                 payment_term_id = self._get_payment_term(cursor, user, picking)
749             else:
750                 account_id = partner.property_account_payable.id
751
752             address_contact_id, address_invoice_id = \
753                     self._get_address_invoice(cursor, user, picking).values()
754
755             comment = self._get_comment_invoice(cursor, user, picking)
756             if group and partner.id in invoices_group:
757                 invoice_id = invoices_group[partner.id]
758                 invoice = invoice_obj.browse(cursor, user, invoice_id)
759                 invoice_vals = {
760                     'name': (invoice.name or '') + ', ' + (picking.name or ''),
761                     'origin': (invoice.origin or '') + ', ' + (picking.name or '') + (picking.origin and (':' + picking.origin) or ''),
762                     'comment': (comment and (invoice.comment and invoice.comment+"\n"+comment or comment)) or (invoice.comment and invoice.comment or ''),
763                 }
764                 invoice_obj.write(cursor, user, [invoice_id], invoice_vals, context=context)
765             else:
766                 invoice_vals = {
767                     'name': picking.name,
768                     'origin': (picking.name or '') + (picking.origin and (':' + picking.origin) or ''),
769                     'type': type,
770                     'account_id': account_id,
771                     'partner_id': partner.id,
772                     'address_invoice_id': address_invoice_id,
773                     'address_contact_id': address_contact_id,
774                     'comment': comment,
775                     'payment_term': payment_term_id,
776                     'fiscal_position': partner.property_account_position.id
777                     }
778                 cur_id = self.get_currency_id(cursor, user, picking)
779                 if cur_id:
780                     invoice_vals['currency_id'] = cur_id
781                 if journal_id:
782                     invoice_vals['journal_id'] = journal_id
783                 invoice_id = invoice_obj.create(cursor, user, invoice_vals,
784                         context=context)
785                 invoices_group[partner.id] = invoice_id
786             res[picking.id] = invoice_id
787             for move_line in picking.move_lines:
788                 if move_line.state == 'cancel':
789                     continue
790                 origin = move_line.picking_id.name
791                 if move_line.picking_id.origin:
792                     origin += ':' + move_line.picking_id.origin
793                 if group:
794                     name = (picking.name or '') + '-' + move_line.name
795                 else:
796                     name = move_line.name
797
798                 if type in ('out_invoice', 'out_refund'):
799                     account_id = move_line.product_id.product_tmpl_id.\
800                             property_account_income.id
801                     if not account_id:
802                         account_id = move_line.product_id.categ_id.\
803                                 property_account_income_categ.id
804                 else:
805                     account_id = move_line.product_id.product_tmpl_id.\
806                             property_account_expense.id
807                     if not account_id:
808                         account_id = move_line.product_id.categ_id.\
809                                 property_account_expense_categ.id
810
811                 price_unit = self._get_price_unit_invoice(cursor, user,
812                         move_line, type)
813                 discount = self._get_discount_invoice(cursor, user, move_line)
814                 tax_ids = self._get_taxes_invoice(cursor, user, move_line, type)
815                 account_analytic_id = self._get_account_analytic_invoice(cursor,
816                         user, picking, move_line)
817
818                 #set UoS if it's a sale and the picking doesn't have one
819                 uos_id = move_line.product_uos and move_line.product_uos.id or False
820                 if not uos_id and type in ('out_invoice', 'out_refund'):
821                     uos_id = move_line.product_uom.id
822
823                 account_id = self.pool.get('account.fiscal.position').map_account(cursor, user, partner.property_account_position, account_id)
824                 invoice_line_id = invoice_line_obj.create(cursor, user, {
825                     'name': name,
826                     'origin': origin,
827                     'invoice_id': invoice_id,
828                     'uos_id': uos_id,
829                     'product_id': move_line.product_id.id,
830                     'account_id': account_id,
831                     'price_unit': price_unit,
832                     'discount': discount,
833                     'quantity': move_line.product_uos_qty or move_line.product_qty,
834                     'invoice_line_tax_id': [(6, 0, tax_ids)],
835                     'account_analytic_id': account_analytic_id,
836                     }, context=context)
837                 self._invoice_line_hook(cursor, user, move_line, invoice_line_id)
838
839             invoice_obj.button_compute(cursor, user, [invoice_id], context=context,
840                     set_total=(type in ('in_invoice', 'in_refund')))
841             self.write(cursor, user, [picking.id], {
842                 'invoice_state': 'invoiced',
843                 }, context=context)
844             self._invoice_hook(cursor, user, picking, invoice_id)
845         self.write(cursor, user, res.keys(), {
846             'invoice_state': 'invoiced',
847             }, context=context)
848         return res
849
850     def test_cancel(self, cr, uid, ids, context={}):
851         for pick in self.browse(cr, uid, ids, context=context):
852             for move in pick.move_lines:
853                 if move.state not in ('cancel',):
854                     return False
855         return True
856
857     def unlink(self, cr, uid, ids, context=None):
858         move_obj = self.pool.get('stock.move')
859         if context is None:
860             context = {}
861         for pick in self.browse(cr, uid, ids, context=context):
862             if pick.state in ['done','cancel']:
863                 raise osv.except_osv(_('Error'), _('You cannot remove the picking which is in %s state !')%(pick.state,))
864             elif pick.state in ['confirmed','assigned', 'draft']:
865                 ids2 = [move.id for move in pick.move_lines]
866                 ctx = context.copy()
867                 ctx.update({'call_unlink':True})
868                 if pick.state != 'draft':
869                     #Cancelling the move in order to affect Virtual stock of product
870                     move_obj.action_cancel(cr, uid, ids2, ctx)
871                 #Removing the move
872                 move_obj.unlink(cr, uid, ids2, ctx)
873             
874         return super(stock_picking, self).unlink(cr, uid, ids, context=context)
875
876 stock_picking()
877
878
879 class stock_production_lot(osv.osv):
880     def name_get(self, cr, uid, ids, context={}):
881         if not ids:
882             return []
883         reads = self.read(cr, uid, ids, ['name', 'ref'], context)
884         res = []
885         for record in reads:
886             name = record['name']
887             if record['ref']:
888                 name = name + '/' + record['ref']
889             res.append((record['id'], name))
890         return res
891
892     _name = 'stock.production.lot'
893     _description = 'Production lot'
894
895     def _get_stock(self, cr, uid, ids, field_name, arg, context={}):
896         if 'location_id' not in context:
897             locations = self.pool.get('stock.location').search(cr, uid, [('usage', '=', 'internal')], context=context)
898         else:
899             locations = context['location_id'] and [context['location_id']] or []
900
901         if isinstance(ids, (int, long)):
902             ids = [ids]
903
904         res = {}.fromkeys(ids, 0.0)
905
906         if locations:
907             cr.execute('''select
908                     prodlot_id,
909                     sum(name)
910                 from
911                     stock_report_prodlots
912                 where
913                     location_id in %s  and
914                     prodlot_id in  %s
915                 group by
916                     prodlot_id
917             ''', (tuple(locations), tuple(ids)))
918             res.update(dict(cr.fetchall()))
919         return res
920
921     def _stock_search(self, cr, uid, obj, name, args, context):
922         locations = self.pool.get('stock.location').search(cr, uid, [('usage', '=', 'internal')])
923         cr.execute('''select
924                 prodlot_id,
925                 sum(name)
926             from
927                 stock_report_prodlots
928             where
929                 location_id in %s
930             group by
931                 prodlot_id
932             having sum(name) ''' + str(args[0][1]) + ' %s',
933                    (tuple(locations), args[0][2]))
934         res = cr.fetchall()
935         ids = [('id', 'in', map(lambda x: x[0], res))]
936         return ids
937
938     _columns = {
939         'name': fields.char('Serial', size=64, required=True),
940         'ref': fields.char('Internal Ref', size=64),
941         'product_id': fields.many2one('product.product', 'Product', required=True),
942         'date': fields.datetime('Created Date', required=True),
943         'stock_available': fields.function(_get_stock, fnct_search=_stock_search, method=True, type="float", string="Available", select="2"),
944         'revisions': fields.one2many('stock.production.lot.revision', 'lot_id', 'Revisions'),
945     }
946     _defaults = {
947         'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
948         'name': lambda x, y, z, c: x.pool.get('ir.sequence').get(y, z, 'stock.lot.serial'),
949         'product_id': lambda x, y, z, c: c.get('product_id', False),
950     }
951     _sql_constraints = [
952         ('name_ref_uniq', 'unique (name, ref)', 'The serial/ref must be unique !'),
953     ]
954
955 stock_production_lot()
956
957
958 class stock_production_lot_revision(osv.osv):
959     _name = 'stock.production.lot.revision'
960     _description = 'Production lot revisions'
961     _columns = {
962         'name': fields.char('Revision Name', size=64, required=True),
963         'description': fields.text('Description'),
964         'date': fields.date('Revision Date'),
965         'indice': fields.char('Revision', size=16),
966         'author_id': fields.many2one('res.users', 'Author'),
967         'lot_id': fields.many2one('stock.production.lot', 'Production lot', select=True, ondelete='cascade'),
968     }
969
970     _defaults = {
971         'author_id': lambda x, y, z, c: z,
972         'date': lambda *a: time.strftime('%Y-%m-%d'),
973     }
974
975 stock_production_lot_revision()
976
977 # ----------------------------------------------------
978 # Move
979 # ----------------------------------------------------
980
981 #
982 # Fields:
983 #   location_dest_id is only used for predicting futur stocks
984 #
985 class stock_move(osv.osv):
986     def _getSSCC(self, cr, uid, context={}):
987         cr.execute('select id from stock_tracking where create_uid=%s order by id desc limit 1', (uid,))
988         res = cr.fetchone()
989         return (res and res[0]) or False
990     _name = "stock.move"
991     _description = "Stock Move"
992
993     def name_get(self, cr, uid, ids, context={}):
994         res = []
995         for line in self.browse(cr, uid, ids, context):
996             res.append((line.id, (line.product_id.code or '/')+': '+line.location_id.name+' > '+line.location_dest_id.name))
997         return res
998
999     def _check_tracking(self, cr, uid, ids):
1000         for move in self.browse(cr, uid, ids):
1001             if not move.prodlot_id and \
1002                (move.state == 'done' and \
1003                ( \
1004                    (move.product_id.track_production and move.location_id.usage == 'production') or \
1005                    (move.product_id.track_production and move.location_dest_id.usage == 'production') or \
1006                    (move.product_id.track_incoming and move.location_id.usage == 'supplier') or \
1007                    (move.product_id.track_outgoing and move.location_dest_id.usage == 'customer') \
1008                )):
1009                 return False
1010         return True
1011
1012     def _check_product_lot(self, cr, uid, ids):
1013         for move in self.browse(cr, uid, ids):
1014             if move.prodlot_id and move.state == 'done' and (move.prodlot_id.product_id.id != move.product_id.id):
1015                 return False
1016         return True
1017
1018     _columns = {
1019         'name': fields.char('Name', size=64, required=True, select=True),
1020         'priority': fields.selection([('0', 'Not urgent'), ('1', 'Urgent')], 'Priority'),
1021
1022         'date': fields.datetime('Date Created'),
1023         'date_planned': fields.datetime('Date', required=True, help="Scheduled date for the movement of the products or real date if the move is done."),
1024
1025         'product_id': fields.many2one('product.product', 'Product', required=True, select=True),
1026
1027         'product_qty': fields.float('Quantity', required=True),
1028         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
1029         'product_uos_qty': fields.float('Quantity (UOS)'),
1030         'product_uos': fields.many2one('product.uom', 'Product UOS'),
1031         'product_packaging': fields.many2one('product.packaging', 'Packaging'),
1032
1033         'location_id': fields.many2one('stock.location', 'Source Location', required=True, select=True),
1034         'location_dest_id': fields.many2one('stock.location', 'Dest. Location', required=True, select=True),
1035         'address_id': fields.many2one('res.partner.address', 'Dest. Address'),
1036
1037         'prodlot_id': fields.many2one('stock.production.lot', 'Production Lot', help="Production lot is used to put a serial number on the production"),
1038         'tracking_id': fields.many2one('stock.tracking', 'Tracking Lot', select=True, help="Tracking lot is the code that will be put on the logistical unit/pallet"),
1039 #       'lot_id': fields.many2one('stock.lot', 'Consumer lot', select=True, readonly=True),
1040
1041         'auto_validate': fields.boolean('Auto Validate'),
1042
1043         'move_dest_id': fields.many2one('stock.move', 'Dest. Move'),
1044         'move_history_ids': fields.many2many('stock.move', 'stock_move_history_ids', 'parent_id', 'child_id', 'Move History'),
1045         'move_history_ids2': fields.many2many('stock.move', 'stock_move_history_ids', 'child_id', 'parent_id', 'Move History'),
1046         'picking_id': fields.many2one('stock.picking', 'Packing List', select=True),
1047
1048         'note': fields.text('Notes'),
1049
1050         'state': fields.selection([('draft', 'Draft'), ('waiting', 'Waiting'), ('confirmed', 'Confirmed'), ('assigned', 'Available'), ('done', 'Done'), ('cancel', 'Cancelled')], 'Status', readonly=True, select=True),
1051         'price_unit': fields.float('Unit Price',
1052             digits=(16, int(config['price_accuracy']))),
1053     }
1054     _constraints = [
1055         (_check_tracking,
1056             'You must assign a production lot for this product',
1057             ['prodlot_id']),
1058         (_check_product_lot,
1059             'You try to assign a lot which is not from the same product',
1060             ['prodlot_id'])]
1061
1062     def _default_location_destination(self, cr, uid, context={}):
1063         if context.get('move_line', []):
1064             if context['move_line'][0]:
1065                 if isinstance(context['move_line'][0], (tuple, list)):
1066                     return context['move_line'][0][2] and context['move_line'][0][2]['location_dest_id'] or False
1067                 else:
1068                     move_list = self.pool.get('stock.move').read(cr, uid, context['move_line'][0], ['location_dest_id'])
1069                     return move_list and move_list['location_dest_id'][0] or False
1070         if context.get('address_out_id', False):
1071             return self.pool.get('res.partner.address').browse(cr, uid, context['address_out_id'], context).partner_id.property_stock_customer.id
1072         return False
1073
1074     def _default_location_source(self, cr, uid, context={}):
1075         if context.get('move_line', []):
1076             try:
1077                 return context['move_line'][0][2]['location_id']
1078             except:
1079                 pass
1080         if context.get('address_in_id', False):
1081             return self.pool.get('res.partner.address').browse(cr, uid, context['address_in_id'], context).partner_id.property_stock_supplier.id
1082         return False
1083
1084     _defaults = {
1085         'location_id': _default_location_source,
1086         'location_dest_id': _default_location_destination,
1087         'state': lambda *a: 'draft',
1088         'priority': lambda *a: '1',
1089         'product_qty': lambda *a: 1.0,
1090         'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
1091         'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
1092     }
1093
1094     def _auto_init(self, cursor, context):
1095         res = super(stock_move, self)._auto_init(cursor, context)
1096         cursor.execute('SELECT indexname \
1097                 FROM pg_indexes \
1098                 WHERE indexname = \'stock_move_location_id_location_dest_id_product_id_state\'')
1099         if not cursor.fetchone():
1100             cursor.execute('CREATE INDEX stock_move_location_id_location_dest_id_product_id_state \
1101                     ON stock_move (location_id, location_dest_id, product_id, state)')
1102             cursor.commit()
1103         return res
1104
1105     def onchange_lot_id(self, cr, uid, ids, prodlot_id=False, product_qty=False, loc_id=False, context=None):
1106         if not prodlot_id or not loc_id:
1107             return {}
1108         ctx = context and context.copy() or {}
1109         ctx['location_id'] = loc_id
1110         prodlot = self.pool.get('stock.production.lot').browse(cr, uid, prodlot_id, ctx)
1111         location = self.pool.get('stock.location').browse(cr, uid, loc_id)
1112         warning = {}
1113         if (location.usage == 'internal') and (product_qty > (prodlot.stock_available or 0.0)):
1114             warning = {
1115                 'title': _('Bad Lot Assignation !'),
1116                 'message': _('You are moving %.2f products but only %.2f available in this lot.') % (product_qty, prodlot.stock_available or 0.0)
1117             }
1118         return {'warning': warning}
1119
1120     def onchange_quantity(self, cr, uid, ids, product_id, product_qty, product_uom, product_uos):
1121         result = {
1122                   'product_uos_qty': 0.00
1123           }
1124
1125         if (not product_id) or (product_qty <=0.0):
1126             return {'value': result}
1127
1128         product_obj = self.pool.get('product.product')
1129         uos_coeff = product_obj.read(cr, uid, product_id, ['uos_coeff'])
1130
1131         if product_uos and product_uom and (product_uom != product_uos):
1132             result['product_uos_qty'] = product_qty * uos_coeff['uos_coeff']
1133         else:
1134             result['product_uos_qty'] = product_qty
1135
1136         return {'value': result}
1137
1138     def onchange_uos_quantity(self, cr, uid, ids, product_id, product_uos_qty, product_uos, product_uom):
1139         result = {
1140                   'product_qty': 0.00
1141           }
1142
1143         if (not product_id) or (product_uos_qty <=0.0):
1144             return {'value': result}
1145         
1146         product_obj = self.pool.get('product.product')
1147         uos_coeff = product_obj.read(cr, uid, product_id, ['uos_coeff'])
1148     
1149         if product_uos and product_uom and (product_uom != product_uos):
1150             result['product_qty'] = product_uos_qty / uos_coeff['uos_coeff']
1151         else:
1152             result['product_qty'] = product_uos_qty
1153     
1154         return {'value': result}
1155
1156     def onchange_product_id(self, cr, uid, ids, prod_id=False, loc_id=False, loc_dest_id=False, address_id=False):
1157         if not prod_id:
1158             return {}
1159         lang = False
1160         if address_id:
1161             addr_rec = self.pool.get('res.partner.address').browse(cr, uid, address_id)
1162             if addr_rec:
1163                 lang = addr_rec.partner_id and addr_rec.partner_id.lang or False
1164         ctx = {'lang': lang}
1165
1166         product = self.pool.get('product.product').browse(cr, uid, [prod_id], context=ctx)[0]
1167         uos_id  = product.uos_id and product.uos_id.id or False
1168         result = {
1169             'name': product.partner_ref,
1170             'product_uom': product.uom_id.id,
1171             'product_uos': uos_id,
1172             'product_qty': 1.00,
1173             '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']
1174         }
1175
1176         if loc_id:
1177             result['location_id'] = loc_id
1178         if loc_dest_id:
1179             result['location_dest_id'] = loc_dest_id
1180         return {'value': result}
1181
1182     def _chain_compute(self, cr, uid, moves, context={}):
1183         result = {}
1184         for m in moves:
1185             dest = self.pool.get('stock.location').chained_location_get(
1186                 cr,
1187                 uid,
1188                 m.location_dest_id,
1189                 m.picking_id and m.picking_id.address_id and m.picking_id.address_id.partner_id,
1190                 m.product_id,
1191                 context
1192             )
1193             if dest:
1194                 if dest[1] == 'transparent':
1195                     self.write(cr, uid, [m.id], {
1196                         'date_planned': (DateTime.strptime(m.date_planned, '%Y-%m-%d %H:%M:%S') + \
1197                             DateTime.RelativeDateTime(days=dest[2] or 0)).strftime('%Y-%m-%d'),
1198                         'location_dest_id': dest[0].id})
1199                 else:
1200                     result.setdefault(m.picking_id, [])
1201                     result[m.picking_id].append( (m, dest) )
1202         return result
1203
1204     def action_confirm(self, cr, uid, ids, context={}):
1205 #        ids = map(lambda m: m.id, moves)
1206         moves = self.browse(cr, uid, ids)
1207         self.write(cr, uid, ids, {'state': 'confirmed'})
1208         i = 0
1209
1210         def create_chained_picking(self, cr, uid, moves, context):
1211             new_moves = []
1212             picking_obj = self.pool.get('stock.picking')
1213             move_obj = self.pool.get('stock.move')
1214             for picking, todo in self._chain_compute(cr, uid, moves, context).items():
1215                 ptype = self.pool.get('stock.location').picking_type_get(cr, uid, todo[0][0].location_dest_id, todo[0][1][0])
1216                 check_picking_ids = picking_obj.search(cr, uid, [('name','=',picking.name),('origin','=',tools.ustr(picking.origin or '')),('type','=',ptype),('move_type','=',picking.move_type)])
1217                 if check_picking_ids:
1218                     pickid = check_picking_ids[0]
1219                 else:
1220                     if picking:
1221                         pickid = picking_obj.create(cr, uid, {
1222                             'name': picking.name,
1223                             'origin': tools.ustr(picking.origin or ''),
1224                             'type': ptype,
1225                             'note': picking.note,
1226                             'move_type': picking.move_type,
1227                             'auto_picking': todo[0][1][1] == 'auto',
1228                             'address_id': picking.address_id.id,
1229                             'invoice_state': 'none'
1230                         })
1231                     else:
1232                         pickid = False
1233                 for move, (loc, auto, delay) in todo:
1234                     # Is it smart to copy ? May be it's better to recreate ?
1235                     new_id = move_obj.copy(cr, uid, move.id, {
1236                         'location_id': move.location_dest_id.id,
1237                         'location_dest_id': loc.id,
1238                         'date_moved': time.strftime('%Y-%m-%d'),
1239                         'picking_id': pickid,
1240                         'state': 'waiting',
1241                         'move_history_ids': [],
1242                         'date_planned': (DateTime.strptime(move.date_planned, '%Y-%m-%d %H:%M:%S') + DateTime.RelativeDateTime(days=delay or 0)).strftime('%Y-%m-%d'),
1243                         'move_history_ids2': []}
1244                     )
1245                     move_obj.write(cr, uid, [move.id], {
1246                         'move_dest_id': new_id,
1247                         'move_history_ids': [(4, new_id)]
1248                     })
1249                     new_moves.append(self.browse(cr, uid, [new_id])[0])
1250                 if pickid:
1251                     wf_service = netsvc.LocalService("workflow")
1252                     wf_service.trg_validate(uid, 'stock.picking', pickid, 'button_confirm', cr)
1253             if new_moves:
1254                 create_chained_picking(self, cr, uid, new_moves, context)
1255         create_chained_picking(self, cr, uid, moves, context)
1256         return []
1257
1258     def action_assign(self, cr, uid, ids, *args):
1259         todo = []
1260         for move in self.browse(cr, uid, ids):
1261             if move.state in ('confirmed', 'waiting'):
1262                 todo.append(move.id)
1263         res = self.check_assign(cr, uid, todo)
1264         return res
1265
1266     def force_assign(self, cr, uid, ids, context={}):
1267         self.write(cr, uid, ids, {'state': 'assigned'})
1268         return True
1269
1270     def cancel_assign(self, cr, uid, ids, context={}):
1271         self.write(cr, uid, ids, {'state': 'confirmed'})
1272         return True
1273
1274     #
1275     # Duplicate stock.move
1276     #
1277     def check_assign(self, cr, uid, ids, context={}):
1278         done = []
1279         count = 0
1280         pickings = {}
1281         for move in self.browse(cr, uid, ids):
1282             if move.product_id.type == 'consu':
1283                 if move.state in ('confirmed', 'waiting'):
1284                     done.append(move.id)
1285                 pickings[move.picking_id.id] = 1
1286                 continue
1287             if move.state in ('confirmed', 'waiting'):
1288                 # Important: we must pass lock=True to _product_reserve() to avoid race conditions and double reservations
1289                 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)
1290                 if res:
1291                     #_product_available_test depends on the next status for correct functioning
1292                     #the test does not work correctly if the same product occurs multiple times
1293                     #in the same order. This is e.g. the case when using the button 'split in two' of
1294                     #the stock outgoing form
1295                     self.write(cr, uid, move.id, {'state':'assigned'})
1296                     done.append(move.id)
1297                     pickings[move.picking_id.id] = 1
1298                     r = res.pop(0)
1299                     cr.execute('update stock_move set location_id=%s, product_qty=%s where id=%s', (r[1], r[0], move.id))
1300
1301                     while res:
1302                         r = res.pop(0)
1303                         move_id = self.copy(cr, uid, move.id, {'product_qty': r[0], 'location_id': r[1]})
1304                         done.append(move_id)
1305         if done:
1306             count += len(done)
1307             self.write(cr, uid, done, {'state': 'assigned'})
1308
1309         if count:
1310             for pick_id in pickings:
1311                 wf_service = netsvc.LocalService("workflow")
1312                 wf_service.trg_write(uid, 'stock.picking', pick_id, cr)
1313         return count
1314
1315     #
1316     # Cancel move => cancel others move and pickings
1317     #
1318     def action_cancel(self, cr, uid, ids, context={}):
1319         if not len(ids):
1320             return True
1321         pickings = {}
1322         for move in self.browse(cr, uid, ids):
1323             if move.state in ('confirmed', 'waiting', 'assigned', 'draft'):
1324                 if move.picking_id:
1325                     pickings[move.picking_id.id] = True
1326             if move.move_dest_id and move.move_dest_id.state == 'waiting':
1327                 self.write(cr, uid, [move.move_dest_id.id], {'state': 'assigned'})
1328                 if context.get('call_unlink',False) and move.move_dest_id.picking_id:
1329                     wf_service = netsvc.LocalService("workflow")
1330                     wf_service.trg_write(uid, 'stock.picking', move.move_dest_id.picking_id.id, cr)
1331         self.write(cr, uid, ids, {'state': 'cancel', 'move_dest_id': False})
1332         if not context.get('call_unlink',False):
1333             for pick in self.pool.get('stock.picking').browse(cr, uid, pickings.keys()):
1334                 if all(move.state == 'cancel' for move in pick.move_lines):
1335                     self.pool.get('stock.picking').write(cr, uid, [pick.id], {'state': 'cancel'})
1336
1337         wf_service = netsvc.LocalService("workflow")
1338         for id in ids:
1339             wf_service.trg_trigger(uid, 'stock.move', id, cr)
1340         #self.action_cancel(cr,uid, ids2, context)
1341         return True
1342
1343     def action_done(self, cr, uid, ids, context=None):
1344         track_flag = False
1345         move_ids = []
1346         for move in self.browse(cr, uid, ids):
1347             if move.move_dest_id.id and (move.state != 'done'):
1348                 cr.execute('insert into stock_move_history_ids (parent_id,child_id) values (%s,%s)', (move.id, move.move_dest_id.id))
1349                 if move.move_dest_id.state in ('waiting', 'confirmed'):
1350                     self.write(cr, uid, [move.move_dest_id.id], {'state': 'assigned'})
1351                     if move.move_dest_id.picking_id:
1352                         wf_service = netsvc.LocalService("workflow")
1353                         wf_service.trg_write(uid, 'stock.picking', move.move_dest_id.picking_id.id, cr)
1354                     else:
1355                         pass
1356                         # self.action_done(cr, uid, [move.move_dest_id.id])
1357                     if move.move_dest_id.auto_validate:
1358                         self.action_done(cr, uid, [move.move_dest_id.id], context=context)
1359
1360             #
1361             # Accounting Entries
1362             #
1363             if move.state in ['done','cancel']:
1364                 continue
1365             move_ids.append(move.id)
1366             acc_src = None
1367             acc_dest = None
1368             if move.location_id.account_id:
1369                 acc_src = move.location_id.account_id.id
1370             if move.location_dest_id.account_id:
1371                 acc_dest = move.location_dest_id.account_id.id
1372             if acc_src or acc_dest:
1373                 test = [('product.product', move.product_id.id)]
1374                 if move.product_id.categ_id:
1375                     test.append( ('product.category', move.product_id.categ_id.id) )
1376                 if not acc_src:
1377                     acc_src = move.product_id.product_tmpl_id.\
1378                             property_stock_account_input.id
1379                     if not acc_src:
1380                         acc_src = move.product_id.categ_id.\
1381                                 property_stock_account_input_categ.id
1382                     if not acc_src:
1383                         raise osv.except_osv(_('Error!'),
1384                                 _('There is no stock input account defined ' \
1385                                         'for this product: "%s" (id: %d)') % \
1386                                         (move.product_id.name,
1387                                             move.product_id.id,))
1388                 if not acc_dest:
1389                     acc_dest = move.product_id.product_tmpl_id.\
1390                             property_stock_account_output.id
1391                     if not acc_dest:
1392                         acc_dest = move.product_id.categ_id.\
1393                                 property_stock_account_output_categ.id
1394                     if not acc_dest:
1395                         raise osv.except_osv(_('Error!'),
1396                                 _('There is no stock output account defined ' \
1397                                         'for this product: "%s" (id: %d)') % \
1398                                         (move.product_id.name,
1399                                             move.product_id.id,))
1400                 if not move.product_id.categ_id.property_stock_journal.id:
1401                     raise osv.except_osv(_('Error!'),
1402                         _('There is no journal defined '\
1403                             'on the product category: "%s" (id: %d)') % \
1404                             (move.product_id.categ_id.name,
1405                                 move.product_id.categ_id.id,))
1406                 journal_id = move.product_id.categ_id.property_stock_journal.id
1407                 if acc_src != acc_dest:
1408                     ref = move.picking_id and move.picking_id.name or False
1409                     product_uom_obj = self.pool.get('product.uom')
1410                     default_uom = move.product_id.uom_id.id
1411                     q = product_uom_obj._compute_qty(cr, uid, move.product_uom.id, move.product_qty, default_uom)
1412                     if move.product_id.cost_method == 'average' and move.price_unit:
1413                         amount = q * move.price_unit
1414                     else:
1415                         amount = q * move.product_id.standard_price
1416
1417                     date = time.strftime('%Y-%m-%d')
1418                     partner_id = False
1419                     if move.picking_id:
1420                         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) or False
1421                     lines = [
1422                             (0, 0, {
1423                                 'name': move.name,
1424                                 'quantity': move.product_qty,
1425                                 'product_id': move.product_id and move.product_id.id or False,
1426                                 'credit': amount,
1427                                 'account_id': acc_src,
1428                                 'ref': ref,
1429                                 'date': date,
1430                                 'partner_id': partner_id}),
1431                             (0, 0, {
1432                                 'name': move.name,
1433                                 'product_id': move.product_id and move.product_id.id or False,
1434                                 'quantity': move.product_qty,
1435                                 'debit': amount,
1436                                 'account_id': acc_dest,
1437                                 'ref': ref,
1438                                 'date': date,
1439                                 'partner_id': partner_id})
1440                     ]
1441                     self.pool.get('account.move').create(cr, uid, {
1442                         'name': move.name,
1443                         'journal_id': journal_id,
1444                         'line_id': lines,
1445                         'ref': ref,
1446                     })
1447         if move_ids:
1448             self.write(cr, uid, move_ids, {'state': 'done', 'date_planned': time.strftime('%Y-%m-%d %H:%M:%S')})
1449             wf_service = netsvc.LocalService("workflow")
1450             for id in move_ids:
1451                 wf_service.trg_trigger(uid, 'stock.move', id, cr)
1452         return True
1453
1454     def unlink(self, cr, uid, ids, context=None):
1455         if context is None:
1456             context = {}
1457         ctx = context.copy()
1458         for move in self.browse(cr, uid, ids, context=ctx):
1459             if move.state != 'draft' and not ctx.get('call_unlink',False):
1460                 raise osv.except_osv(_('UserError'),
1461                         _('You can only delete draft moves.'))
1462         return super(stock_move, self).unlink(
1463             cr, uid, ids, context=ctx)
1464
1465 stock_move()
1466
1467
1468 class stock_inventory(osv.osv):
1469     _name = "stock.inventory"
1470     _description = "Inventory"
1471     _columns = {
1472         'name': fields.char('Inventory', size=64, required=True, readonly=True, states={'draft': [('readonly', False)]}),
1473         'date': fields.datetime('Date create', required=True, readonly=True, states={'draft': [('readonly', False)]}),
1474         'date_done': fields.datetime('Date done'),
1475         'inventory_line_id': fields.one2many('stock.inventory.line', 'inventory_id', 'Inventories', readonly=True, states={'draft': [('readonly', False)]}),
1476         'move_ids': fields.many2many('stock.move', 'stock_inventory_move_rel', 'inventory_id', 'move_id', 'Created Moves'),
1477         'state': fields.selection( (('draft', 'Draft'), ('done', 'Done')), 'Status', readonly=True),
1478     }
1479     _defaults = {
1480         'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
1481         'state': lambda *a: 'draft',
1482     }
1483
1484     #
1485     # Update to support tracking
1486     #
1487     def action_done(self, cr, uid, ids, context=None):
1488         for inv in self.browse(cr, uid, ids):
1489             move_ids = []
1490             move_line = []
1491             for line in inv.inventory_line_id:
1492                 pid = line.product_id.id
1493                 price = line.product_id.standard_price or 0.0
1494                 amount = self.pool.get('stock.location')._product_get(cr, uid, line.location_id.id, [pid], {'uom': line.product_uom.id})[pid]
1495                 change = line.product_qty - amount
1496                 if change:
1497                     location_id = line.product_id.product_tmpl_id.property_stock_inventory.id
1498                     value = {
1499                         'name': 'INV:' + str(line.inventory_id.id) + ':' + line.inventory_id.name,
1500                         'product_id': line.product_id.id,
1501                         'product_uom': line.product_uom.id,
1502                         'date': inv.date,
1503                         'date_planned': inv.date,
1504                         'state': 'assigned'
1505                     }
1506                     if change > 0:
1507                         value.update( {
1508                             'product_qty': change,
1509                             'location_id': location_id,
1510                             'location_dest_id': line.location_id.id,
1511                         })
1512                     else:
1513                         value.update( {
1514                             'product_qty': -change,
1515                             'location_id': line.location_id.id,
1516                             'location_dest_id': location_id,
1517                         })
1518                     move_ids.append(self.pool.get('stock.move').create(cr, uid, value))
1519             if len(move_ids):
1520                 self.pool.get('stock.move').action_done(cr, uid, move_ids,
1521                         context=context)
1522             self.write(cr, uid, [inv.id], {'state': 'done', 'date_done': time.strftime('%Y-%m-%d %H:%M:%S'), 'move_ids': [(6, 0, move_ids)]})
1523         return True
1524
1525     def action_cancel(self, cr, uid, ids, context={}):
1526         for inv in self.browse(cr, uid, ids):
1527             self.pool.get('stock.move').action_cancel(cr, uid, [x.id for x in inv.move_ids], context)
1528             self.write(cr, uid, [inv.id], {'state': 'draft'})
1529         return True
1530
1531 stock_inventory()
1532
1533
1534 class stock_inventory_line(osv.osv):
1535     _name = "stock.inventory.line"
1536     _description = "Inventory line"
1537     _columns = {
1538         'inventory_id': fields.many2one('stock.inventory', 'Inventory', ondelete='cascade', select=True),
1539         'location_id': fields.many2one('stock.location', 'Location', required=True),
1540         'product_id': fields.many2one('product.product', 'Product', required=True),
1541         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
1542         'product_qty': fields.float('Quantity')
1543     }
1544
1545     def on_change_product_id(self, cr, uid, ids, location_id, product, uom=False):
1546         if not product:
1547             return {}
1548         if not uom:
1549             prod = self.pool.get('product.product').browse(cr, uid, [product], {'uom': uom})[0]
1550             uom = prod.uom_id.id
1551         amount = self.pool.get('stock.location')._product_get(cr, uid, location_id, [product], {'uom': uom})[product]
1552         result = {'product_qty': amount, 'product_uom': uom}
1553         return {'value': result}
1554
1555 stock_inventory_line()
1556
1557
1558 #----------------------------------------------------------
1559 # Stock Warehouse
1560 #----------------------------------------------------------
1561 class stock_warehouse(osv.osv):
1562     _name = "stock.warehouse"
1563     _description = "Warehouse"
1564     _columns = {
1565         'name': fields.char('Name', size=60, required=True),
1566 #       'partner_id': fields.many2one('res.partner', 'Owner'),
1567         'partner_address_id': fields.many2one('res.partner.address', 'Owner Address'),
1568         'lot_input_id': fields.many2one('stock.location', 'Location Input', required=True, domain=[('usage','<>','view')]),
1569         'lot_stock_id': fields.many2one('stock.location', 'Location Stock', required=True, domain=[('usage','<>','view')]),
1570         'lot_output_id': fields.many2one('stock.location', 'Location Output', required=True, domain=[('usage','<>','view')]),
1571     }
1572
1573 stock_warehouse()
1574
1575
1576 # Move wizard :
1577 #    get confirm or assign stock move lines of partner and put in current picking.
1578 class stock_picking_move_wizard(osv.osv_memory):
1579     _name = 'stock.picking.move.wizard'
1580
1581     def _get_picking(self, cr, uid, ctx):
1582         if ctx.get('action_id', False):
1583             return ctx['action_id']
1584         return False
1585
1586     def _get_picking_address(self, cr, uid, ctx):
1587         picking_obj = self.pool.get('stock.picking')
1588         if ctx.get('action_id', False):
1589             picking = picking_obj.browse(cr, uid, [ctx['action_id']])[0]
1590             return picking.address_id and picking.address_id.id or False
1591         return False
1592
1593     _columns = {
1594         'name': fields.char('Name', size=64, invisible=True),
1595         #'move_lines': fields.one2many('stock.move', 'picking_id', 'Move lines',readonly=True),
1596         'move_ids': fields.many2many('stock.move', 'picking_move_wizard_rel', 'picking_move_wizard_id', 'move_id', 'Move lines', required=True),
1597         'address_id': fields.many2one('res.partner.address', 'Dest. Address', invisible=True),
1598         'picking_id': fields.many2one('stock.picking', 'Packing list', select=True, invisible=True),
1599     }
1600     _defaults = {
1601         'picking_id': _get_picking,
1602         'address_id': _get_picking_address,
1603     }
1604
1605     def action_move(self, cr, uid, ids, context=None):
1606         move_obj = self.pool.get('stock.move')
1607         picking_obj = self.pool.get('stock.picking')
1608         for act in self.read(cr, uid, ids):
1609             move_lines = move_obj.browse(cr, uid, act['move_ids'])
1610             for line in move_lines:
1611                 if line.picking_id:
1612                     picking_obj.write(cr, uid, [line.picking_id.id], {'move_lines': [(1, line.id, {'picking_id': act['picking_id']})]})
1613                     picking_obj.write(cr, uid, [act['picking_id']], {'move_lines': [(1, line.id, {'picking_id': act['picking_id']})]})
1614                     cr.commit()
1615                     old_picking = picking_obj.read(cr, uid, [line.picking_id.id])[0]
1616                     if not len(old_picking['move_lines']):
1617                         picking_obj.write(cr, uid, [old_picking['id']], {'state': 'done'})
1618                 else:
1619                     raise osv.except_osv(_('UserError'),
1620                         _('You can not create new moves.'))
1621         return {'type': 'ir.actions.act_window_close'}
1622
1623 stock_picking_move_wizard()
1624
1625
1626 class report_stock_lines_date(osv.osv):
1627     _name = "report.stock.lines.date"
1628     _description = "Dates of Inventories"
1629     _auto = False
1630     _columns = {
1631         'product_id': fields.many2one('product.product', 'Product Id', readonly=True),
1632         'create_date': fields.datetime('Latest Date of Inventory'),
1633         }
1634
1635     def init(self, cr):
1636         cr.execute("""
1637             create or replace view report_stock_lines_date as (
1638                 select
1639                 p.id as id,
1640                 p.id as product_id,
1641                 max(l.create_date) as create_date
1642                 from
1643                 product_product p
1644                 left outer join
1645                 stock_inventory_line l on (p.id=l.product_id)
1646                 where l.create_date is not null
1647                 group by p.id
1648             )""")
1649
1650 report_stock_lines_date()