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