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