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