7848dc2551e51b90c02b9d2397d63d2de44ee876
[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,select=1),
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', context=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,select=1), 
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', context=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',select=1),
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 class stock_split_production_lots(osv.osv_memory):
905     _name = "stock.split.production.lots"
906     _description = "Split Production Lots"
907     
908     def _quantity_default_get(self, cr, uid, object=False, field=False, context=None):
909         cr.execute("select %s from %s where id=%s" %(field,object,context.get('active_id')))
910         res = cr.fetchone()[0]
911         return res
912     
913     _columns = {
914         'name': fields.char('Lot Number/Prefix', size=64, required=True),
915         'qty': fields.float('Total Quantity'),
916         'action': fields.selection([('split','Split'),('keepinone','Keep in one lot')],'Action'),
917     }
918     _defaults = {
919                  'qty': lambda self,cr,uid,c: self.pool.get('stock.split.production.lots')._quantity_default_get(cr, uid, 'stock_move', 'product_qty',context=c) or 1,
920     }
921     
922     def split_lines(self, cr, uid, ids, context={}):
923         data = self.read(cr, uid, ids[0])
924         prodlot_obj = self.pool.get('stock.production.lot')
925         move_obj = self.pool.get('stock.move')
926         move_browse = move_obj.browse(cr, uid, context['active_id'])
927         
928         quantity = data['qty']
929         if quantity <= 0 or move_browse.product_qty == 0:
930             return {}
931         uos_qty = quantity/move_browse.product_qty*move_browse.product_uos_qty
932
933         quantity_rest = move_browse.product_qty%quantity
934         uos_qty_rest = quantity_rest/move_browse.product_qty*move_browse.product_uos_qty
935     
936         update_val = {
937             'product_qty': quantity,
938             'product_uos_qty': uos_qty,
939         }
940         
941         new_move = []
942         for idx in range(int(move_browse.product_qty//quantity)):
943             if idx:
944                 current_move = move_obj.copy(cr, uid, move_browse.id, {'state': move_browse.state})
945                 new_move.append(current_move)
946             else:
947                 current_move = move_browse.id
948             new_prodlot = prodlot_obj.create(cr, uid, {'name': data['name'], 'ref': '%d'%idx}, {'product_id': move_browse.product_id.id})
949             update_val['prodlot_id'] = new_prodlot
950             move_obj.write(cr, uid, [current_move], update_val)
951         
952         if quantity_rest > 0:
953             idx = int(move_browse.product_qty//quantity)
954             update_val['product_qty'] = quantity_rest
955             update_val['product_uos_qty'] = uos_qty_rest
956             if idx:
957                 current_move = move_obj.copy(cr, uid, move_browse.id, {'state': move_browse.state})
958                 new_move.append(current_move)
959             else:
960                 current_move = move_browse.id
961             new_prodlot = prodlot_obj.create(cr, uid, {'name': data['name'], 'ref': '%d'%idx}, {'product_id': move_browse.product_id.id})
962             update_val['prodlot_id'] = new_prodlot
963             move_obj.write(cr, uid, [current_move], update_val)
964         
965         return {}
966
967 stock_split_production_lots()
968
969
970 class stock_production_lot_revision(osv.osv):
971     _name = 'stock.production.lot.revision'
972     _description = 'Production lot revisions'
973     _columns = {
974         'name': fields.char('Revision Name', size=64, required=True),
975         'description': fields.text('Description'),
976         'date': fields.date('Revision Date'),
977         'indice': fields.char('Revision', size=16),
978         'author_id': fields.many2one('res.users', 'Author'),
979         'lot_id': fields.many2one('stock.production.lot', 'Production lot', select=True, ondelete='cascade'),
980         'company_id': fields.related('lot_id','company_id',type='many2one',relation='res.company',string='Company',store=True),
981     }
982
983     _defaults = {
984         'author_id': lambda x, y, z, c: z,
985         'date': lambda *a: time.strftime('%Y-%m-%d'),
986     }
987
988 stock_production_lot_revision()
989
990 # ----------------------------------------------------
991 # Move
992 # ----------------------------------------------------
993
994 #
995 # Fields:
996 #   location_dest_id is only used for predicting futur stocks
997 #
998 class stock_move(osv.osv):
999     def _getSSCC(self, cr, uid, context={}):
1000         cr.execute('select id from stock_tracking where create_uid=%s order by id desc limit 1', (uid,))
1001         res = cr.fetchone()
1002         return (res and res[0]) or False
1003     _name = "stock.move"
1004     _description = "Stock Move"
1005
1006     def name_get(self, cr, uid, ids, context={}):
1007         res = []
1008         for line in self.browse(cr, uid, ids, context):
1009             res.append((line.id, (line.product_id.code or '/')+': '+line.location_id.name+' > '+line.location_dest_id.name))
1010         return res
1011
1012     def _check_tracking(self, cr, uid, ids):
1013         for move in self.browse(cr, uid, ids):
1014             if not move.prodlot_id and \
1015                (move.state == 'done' and \
1016                ( \
1017                    (move.product_id.track_production and move.location_id.usage=='production') or \
1018                    (move.product_id.track_production and move.location_dest_id.usage=='production') or \
1019                    (move.product_id.track_incoming and move.location_id.usage=='supplier') or \
1020                    (move.product_id.track_outgoing and move.location_dest_id.usage=='customer') \
1021                )):
1022                 return False
1023         return True
1024
1025     def _check_product_lot(self, cr, uid, ids):
1026         for move in self.browse(cr, uid, ids):
1027             if move.prodlot_id and (move.prodlot_id.product_id.id != move.product_id.id):
1028                 return False
1029         return True
1030
1031     _columns = {
1032         'name': fields.char('Name', size=64, required=True, select=True),
1033         'priority': fields.selection([('0', 'Not urgent'), ('1', 'Urgent')], 'Priority'),
1034
1035         'date': fields.datetime('Date Created'),
1036         'date_planned': fields.datetime('Date', required=True, help="Scheduled date for the movement of the products or real date if the move is done."),
1037
1038         'product_id': fields.many2one('product.product', 'Product', required=True, select=True),
1039
1040         'product_qty': fields.float('Quantity', required=True),
1041         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
1042         'product_uos_qty': fields.float('Quantity (UOS)'),
1043         'product_uos': fields.many2one('product.uom', 'Product UOS'),
1044         'product_packaging': fields.many2one('product.packaging', 'Packaging', help="It specifies attributes of packaging like type, quantity of packaging,etc."),
1045
1046         '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."),
1047         'location_dest_id': fields.many2one('stock.location', 'Dest. Location', required=True, select=True, help="Location where the system will stock the finished products."),
1048         'address_id': fields.many2one('res.partner.address', 'Dest. Address', help="Address where goods are to be delivered"),
1049
1050         'prodlot_id': fields.many2one('stock.production.lot', 'Production Lot', help="Production lot is used to put a serial number on the production"),
1051         '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"),
1052 #       'lot_id': fields.many2one('stock.lot', 'Consumer lot', select=True, readonly=True),
1053
1054         'auto_validate': fields.boolean('Auto Validate'),
1055
1056         'move_dest_id': fields.many2one('stock.move', 'Dest. Move'),
1057         'move_history_ids': fields.many2many('stock.move', 'stock_move_history_ids', 'parent_id', 'child_id', 'Move History'),
1058         'move_history_ids2': fields.many2many('stock.move', 'stock_move_history_ids', 'child_id', 'parent_id', 'Move History'),
1059         'picking_id': fields.many2one('stock.picking', 'Picking List', select=True),
1060
1061         'note': fields.text('Notes'),
1062
1063         'state': fields.selection([('draft', 'Draft'), ('waiting', 'Waiting'), ('confirmed', 'Confirmed'), ('assigned', 'Available'), ('done', 'Done'), ('cancel', 'Cancelled')], 'State', readonly=True, select=True,
1064                                   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\'.\
1065                                   \nThe state is \'Waiting\' if the move is waiting for another one.'),
1066         'price_unit': fields.float('Unit Price',
1067             digits=(16, int(config['price_accuracy']))),
1068         'company_id': fields.many2one('res.company', 'Company', required=True,select=1),
1069         'partner_id': fields.related('picking_id','address_id','partner_id',type='many2one', relation="res.partner", string="Partner"),
1070         'backorder_id': fields.related('picking_id','backorder_id',type='many2one', relation="stock.picking", string="Back Orders"), 
1071         'origin': fields.related('picking_id','origin',type='char', size=64, relation="stock.picking", string="Origin"),
1072         'move_stock_return_history': fields.many2many('stock.move', 'stock_move_return_history', 'move_id', 'return_move_id', 'Move Return History',readonly=True),           
1073     }
1074     _constraints = [
1075         (_check_tracking,
1076             'You must assign a production lot for this product',
1077             ['prodlot_id']),
1078         (_check_product_lot,
1079             'You try to assign a lot which is not from the same product',
1080             ['prodlot_id'])]
1081
1082     def _default_location_destination(self, cr, uid, context={}):
1083         if context.get('move_line', []):
1084             if context['move_line'][0]:
1085                 if isinstance(context['move_line'][0], (tuple, list)):
1086                     return context['move_line'][0][2] and context['move_line'][0][2]['location_dest_id'] or False
1087                 else:
1088                     move_list = self.pool.get('stock.move').read(cr, uid, context['move_line'][0], ['location_dest_id'])
1089                     return move_list and move_list['location_dest_id'][0] or False
1090         if context.get('address_out_id', False):
1091             return self.pool.get('res.partner.address').browse(cr, uid, context['address_out_id'], context).partner_id.property_stock_customer.id
1092         return False
1093
1094     def _default_location_source(self, cr, uid, context={}):
1095         if context.get('move_line', []):
1096             try:
1097                 return context['move_line'][0][2]['location_id']
1098             except:
1099                 pass
1100         if context.get('address_in_id', False):
1101             return self.pool.get('res.partner.address').browse(cr, uid, context['address_in_id'], context).partner_id.property_stock_supplier.id
1102         return False
1103
1104     _defaults = {
1105         'location_id': _default_location_source,
1106         'location_dest_id': _default_location_destination,
1107         'state': lambda *a: 'draft',
1108         'priority': lambda *a: '1',
1109         'product_qty': lambda *a: 1.0,
1110         'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
1111         'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
1112         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.move', context=c)
1113     }
1114     
1115     def copy(self, cr, uid, id, default=None, context={}):
1116         if default is None:
1117             default = {}
1118         default = default.copy()
1119         default['move_stock_return_history'] = []
1120         return super(stock_move, self).copy(cr, uid, id, default, context)
1121     
1122     def create(self, cr, user, vals, context=None):
1123         if vals.get('move_stock_return_history',False):
1124             vals['move_stock_return_history'] = []
1125         return super(stock_move, self).create(cr, user, vals, context)
1126     
1127     def _auto_init(self, cursor, context):
1128         res = super(stock_move, self)._auto_init(cursor, context)
1129         cursor.execute('SELECT indexname \
1130                 FROM pg_indexes \
1131                 WHERE indexname = \'stock_move_location_id_location_dest_id_product_id_state\'')
1132         if not cursor.fetchone():
1133             cursor.execute('CREATE INDEX stock_move_location_id_location_dest_id_product_id_state \
1134                     ON stock_move (location_id, location_dest_id, product_id, state)')
1135             cursor.commit()
1136         return res
1137
1138     def onchange_lot_id(self, cr, uid, ids, prodlot_id=False, product_qty=False, loc_id=False, context=None):
1139         if not prodlot_id or not loc_id:
1140             return {}
1141         ctx = context and context.copy() or {}
1142         ctx['location_id'] = loc_id
1143         prodlot = self.pool.get('stock.production.lot').browse(cr, uid, prodlot_id, ctx)
1144         location = self.pool.get('stock.location').browse(cr, uid, loc_id)
1145         warning = {}
1146         if (location.usage == 'internal') and (product_qty > (prodlot.stock_available or 0.0)):
1147             warning = {
1148                 'title': 'Bad Lot Assignation !',
1149                 'message': 'You are moving %.2f products but only %.2f available in this lot.' % (product_qty, prodlot.stock_available or 0.0)
1150             }
1151         return {'warning': warning}
1152     
1153     def onchange_quantity(self, cr, uid, ids, product_id, product_qty, product_uom, product_uos):
1154         result = {
1155                   'product_uos_qty': 0.00
1156           }
1157         
1158         if (not product_id) or (product_qty <=0.0):
1159             return {'value': result}
1160             
1161         product_obj = self.pool.get('product.product')
1162         uos_coeff = product_obj.read(cr, uid, product_id, ['uos_coeff'])
1163         
1164         if product_uos and product_uom and (product_uom != product_uos):
1165             result['product_uos_qty'] = product_qty * uos_coeff['uos_coeff']
1166         else:
1167             result['product_uos_qty'] = product_qty
1168         
1169         return {'value': result}
1170     
1171     def onchange_product_id(self, cr, uid, ids, prod_id=False, loc_id=False, loc_dest_id=False):
1172         if not prod_id:
1173             return {}
1174         product = self.pool.get('product.product').browse(cr, uid, [prod_id])[0]
1175         uos_id  = product.uos_id and product.uos_id.id or False
1176         result = {
1177             'name': product.partner_ref,
1178             'product_uom': product.uom_id.id,
1179             'product_uos': uos_id,
1180             'product_qty': 1.00,
1181             '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']
1182         }
1183         
1184         if loc_id:
1185             result['location_id'] = loc_id
1186         if loc_dest_id:
1187             result['location_dest_id'] = loc_dest_id
1188         return {'value': result}
1189
1190     def _chain_compute(self, cr, uid, moves, context={}):
1191         result = {}
1192         for m in moves:
1193             dest = self.pool.get('stock.location').chained_location_get(
1194                 cr,
1195                 uid,
1196                 m.location_dest_id,
1197                 m.picking_id and m.picking_id.address_id and m.picking_id.address_id.partner_id,
1198                 m.product_id,
1199                 context
1200             )
1201             if dest:
1202                 if dest[1] == 'transparent':
1203                     self.write(cr, uid, [m.id], {
1204                         'date_planned': (DateTime.strptime(m.date_planned, '%Y-%m-%d %H:%M:%S') + \
1205                             DateTime.RelativeDateTime(days=dest[2] or 0)).strftime('%Y-%m-%d'),
1206                         'location_dest_id': dest[0].id})
1207                 else:
1208                     result.setdefault(m.picking_id, [])
1209                     result[m.picking_id].append( (m, dest) )
1210         return result
1211
1212     def action_confirm(self, cr, uid, ids, context={}):
1213 #        ids = map(lambda m: m.id, moves)
1214         moves = self.browse(cr, uid, ids)
1215         self.write(cr, uid, ids, {'state': 'confirmed'})
1216         i = 0
1217
1218         def create_chained_picking(self, cr, uid, moves, context):
1219             new_moves = []
1220             for picking, todo in self._chain_compute(cr, uid, moves, context).items():
1221                 ptype = self.pool.get('stock.location').picking_type_get(cr, uid, todo[0][0].location_dest_id, todo[0][1][0])
1222                 pickid = self.pool.get('stock.picking').create(cr, uid, {
1223                     'name': picking.name,
1224                     'origin': str(picking.origin or ''),
1225                     'type': ptype,
1226                     'note': picking.note,
1227                     'move_type': picking.move_type,
1228                     'auto_picking': todo[0][1][1] == 'auto',
1229                     'address_id': picking.address_id.id,
1230                     'invoice_state': 'none'
1231                 })
1232                 for move, (loc, auto, delay) in todo:
1233                     # Is it smart to copy ? May be it's better to recreate ?
1234                     new_id = self.pool.get('stock.move').copy(cr, uid, move.id, {
1235                         'location_id': move.location_dest_id.id,
1236                         'location_dest_id': loc.id,
1237                         'date_moved': time.strftime('%Y-%m-%d'),
1238                         'picking_id': pickid,
1239                         'state': 'waiting',
1240                         'move_history_ids': [],
1241                         'date_planned': (DateTime.strptime(move.date_planned, '%Y-%m-%d %H:%M:%S') + DateTime.RelativeDateTime(days=delay or 0)).strftime('%Y-%m-%d'),
1242                         'move_history_ids2': []}
1243                     )
1244                     self.pool.get('stock.move').write(cr, uid, [move.id], {
1245                         'move_dest_id': new_id,
1246                         'move_history_ids': [(4, new_id)]
1247                     })
1248                     new_moves.append(self.browse(cr, uid, [new_id])[0])
1249                 wf_service = netsvc.LocalService("workflow")
1250                 wf_service.trg_validate(uid, 'stock.picking', pickid, 'button_confirm', cr)
1251             if new_moves:
1252                 create_chained_picking(self, cr, uid, new_moves, context)
1253         create_chained_picking(self, cr, uid, moves, context)
1254         return []
1255
1256     def action_assign(self, cr, uid, ids, *args):
1257         todo = []
1258         for move in self.browse(cr, uid, ids):
1259             if move.state in ('confirmed', 'waiting'):
1260                 todo.append(move.id)
1261         res = self.check_assign(cr, uid, todo)
1262         return res
1263
1264     def force_assign(self, cr, uid, ids, context={}):
1265         self.write(cr, uid, ids, {'state': 'assigned'})
1266         return True
1267
1268     def cancel_assign(self, cr, uid, ids, context={}):
1269         self.write(cr, uid, ids, {'state': 'confirmed'})
1270         return True
1271
1272     #
1273     # Duplicate stock.move
1274     #
1275     def check_assign(self, cr, uid, ids, context={}):
1276         done = []
1277         count = 0
1278         pickings = {}
1279         for move in self.browse(cr, uid, ids):
1280             if move.product_id.type == 'consu':
1281                 if move.state in ('confirmed', 'waiting'):
1282                     done.append(move.id)
1283                 pickings[move.picking_id.id] = 1
1284                 continue
1285             if move.state in ('confirmed', 'waiting'):
1286                 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})
1287                 if res:
1288                     #_product_available_test depends on the next status for correct functioning
1289                     #the test does not work correctly if the same product occurs multiple times
1290                     #in the same order. This is e.g. the case when using the button 'split in two' of
1291                     #the stock outgoing form
1292                     self.write(cr, uid, move.id, {'state':'assigned'})
1293                     done.append(move.id)
1294                     pickings[move.picking_id.id] = 1
1295                     r = res.pop(0)
1296                     cr.execute('update stock_move set location_id=%s, product_qty=%s where id=%s', (r[1], r[0], move.id))
1297
1298                     while res:
1299                         r = res.pop(0)
1300                         move_id = self.copy(cr, uid, move.id, {'product_qty': r[0], 'location_id': r[1]})
1301                         done.append(move_id)
1302                         #cr.execute('insert into stock_move_history_ids values (%s,%s)', (move.id,move_id))
1303         if done:
1304             count += len(done)
1305             self.write(cr, uid, done, {'state': 'assigned'})
1306
1307         if count:
1308             for pick_id in pickings:
1309                 wf_service = netsvc.LocalService("workflow")
1310                 wf_service.trg_write(uid, 'stock.picking', pick_id, cr)
1311         return count
1312
1313     #
1314     # Cancel move => cancel others move and pickings
1315     #
1316     def action_cancel(self, cr, uid, ids, context={}):
1317         if not len(ids):
1318             return True
1319         pickings = {}
1320         for move in self.browse(cr, uid, ids):
1321             if move.state in ('confirmed', 'waiting', 'assigned', 'draft'):
1322                 if move.picking_id:
1323                     pickings[move.picking_id.id] = True
1324             if move.move_dest_id and move.move_dest_id.state == 'waiting':
1325                 self.write(cr, uid, [move.move_dest_id.id], {'state': 'assigned'})
1326                 if move.move_dest_id.picking_id:
1327                     wf_service = netsvc.LocalService("workflow")
1328                     wf_service.trg_write(uid, 'stock.picking', move.move_dest_id.picking_id.id, cr)
1329         self.write(cr, uid, ids, {'state': 'cancel', 'move_dest_id': False})
1330
1331         for pick in self.pool.get('stock.picking').browse(cr, uid, pickings.keys()):
1332             if all(move.state == 'cancel' for move in pick.move_lines):
1333                 self.pool.get('stock.picking').write(cr, uid, [pick.id], {'state': 'cancel'})
1334
1335         wf_service = netsvc.LocalService("workflow")
1336         for id in ids:
1337             wf_service.trg_trigger(uid, 'stock.move', id, cr)
1338         #self.action_cancel(cr,uid, ids2, context)
1339         return True
1340
1341     def action_done(self, cr, uid, ids, context=None):
1342         track_flag = False
1343         for move in self.browse(cr, uid, ids):
1344             if move.move_dest_id.id and (move.state != 'done'):
1345                 cr.execute('insert into stock_move_history_ids (parent_id,child_id) values (%s,%s)', (move.id, move.move_dest_id.id))
1346                 if move.move_dest_id.state in ('waiting', 'confirmed'):
1347                     self.write(cr, uid, [move.move_dest_id.id], {'state': 'assigned'})
1348                     if move.move_dest_id.picking_id:
1349                         wf_service = netsvc.LocalService("workflow")
1350                         wf_service.trg_write(uid, 'stock.picking', move.move_dest_id.picking_id.id, cr)
1351                     else:
1352                         pass
1353                         # self.action_done(cr, uid, [move.move_dest_id.id])
1354                     if move.move_dest_id.auto_validate:
1355                         self.action_done(cr, uid, [move.move_dest_id.id], context=context)
1356
1357             #
1358             # Accounting Entries
1359             #
1360             acc_src = None
1361             acc_dest = None
1362             if move.location_id.account_id:
1363                 acc_src = move.location_id.account_id.id
1364             if move.location_dest_id.account_id:
1365                 acc_dest = move.location_dest_id.account_id.id
1366             if acc_src or acc_dest:
1367                 test = [('product.product', move.product_id.id)]
1368                 if move.product_id.categ_id:
1369                     test.append( ('product.category', move.product_id.categ_id.id) )
1370                 if not acc_src:
1371                     acc_src = move.product_id.product_tmpl_id.\
1372                             property_stock_account_input.id
1373                     if not acc_src:
1374                         acc_src = move.product_id.categ_id.\
1375                                 property_stock_account_input_categ.id
1376                     if not acc_src:
1377                         raise osv.except_osv(_('Error!'),
1378                                 _('There is no stock input account defined ' \
1379                                         'for this product: "%s" (id: %d)') % \
1380                                         (move.product_id.name,
1381                                             move.product_id.id,))
1382                 if not acc_dest:
1383                     acc_dest = move.product_id.product_tmpl_id.\
1384                             property_stock_account_output.id
1385                     if not acc_dest:
1386                         acc_dest = move.product_id.categ_id.\
1387                                 property_stock_account_output_categ.id
1388                     if not acc_dest:
1389                         raise osv.except_osv(_('Error!'),
1390                                 _('There is no stock output account defined ' \
1391                                         'for this product: "%s" (id: %d)') % \
1392                                         (move.product_id.name,
1393                                             move.product_id.id,))
1394                 if not move.product_id.categ_id.property_stock_journal.id:
1395                     raise osv.except_osv(_('Error!'),
1396                         _('There is no journal defined '\
1397                             'on the product category: "%s" (id: %d)') % \
1398                             (move.product_id.categ_id.name,
1399                                 move.product_id.categ_id.id,))
1400                 journal_id = move.product_id.categ_id.property_stock_journal.id
1401                 if acc_src != acc_dest:
1402                     ref = move.picking_id and move.picking_id.name or False
1403                     product_uom_obj = self.pool.get('product.uom')
1404                     default_uom = move.product_id.uom_id.id
1405                     q = product_uom_obj._compute_qty(cr, uid, move.product_uom.id, move.product_qty, default_uom)
1406                     if move.product_id.cost_method == 'average' and move.price_unit:
1407                         amount = q * move.price_unit
1408                     else:
1409                         amount = q * move.product_id.standard_price
1410
1411                     date = time.strftime('%Y-%m-%d')
1412                     partner_id = False
1413                     if move.picking_id:
1414                         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
1415                     lines = [
1416                             (0, 0, {
1417                                 'name': move.name,
1418                                 'quantity': move.product_qty,
1419                                 'product_id': move.product_id and move.product_id.id or False,
1420                                 'credit': amount,
1421                                 'account_id': acc_src,
1422                                 'ref': ref,
1423                                 'date': date,
1424                                 'partner_id': partner_id}),
1425                             (0, 0, {
1426                                 'name': move.name,
1427                                 'product_id': move.product_id and move.product_id.id or False,
1428                                 'quantity': move.product_qty,
1429                                 'debit': amount,
1430                                 'account_id': acc_dest,
1431                                 'ref': ref,
1432                                 'date': date,
1433                                 'partner_id': partner_id})
1434                     ]
1435                     self.pool.get('account.move').create(cr, uid, {
1436                         'name': move.name,
1437                         'journal_id': journal_id,
1438                         'line_id': lines,
1439                         'ref': ref,
1440                     })
1441         self.write(cr, uid, ids, {'state': 'done', 'date_planned': time.strftime('%Y-%m-%d %H:%M:%S')})
1442         wf_service = netsvc.LocalService("workflow")
1443         for id in ids:
1444             wf_service.trg_trigger(uid, 'stock.move', id, cr)
1445         return True
1446
1447     def unlink(self, cr, uid, ids, context=None):
1448         for move in self.browse(cr, uid, ids, context=context):
1449             if move.state != 'draft':
1450                 raise osv.except_osv(_('UserError'),
1451                         _('You can only delete draft moves.'))
1452         return super(stock_move, self).unlink(
1453             cr, uid, ids, context=context)
1454
1455 stock_move()
1456
1457
1458 class stock_inventory(osv.osv):
1459     _name = "stock.inventory"
1460     _description = "Inventory"
1461     _columns = {
1462         'name': fields.char('Inventory', size=64, required=True, readonly=True, states={'draft': [('readonly', False)]}),
1463         'date': fields.datetime('Date create', required=True, readonly=True, states={'draft': [('readonly', False)]}),
1464         'date_done': fields.datetime('Date done'),
1465         'inventory_line_id': fields.one2many('stock.inventory.line', 'inventory_id', 'Inventories', readonly=True, states={'draft': [('readonly', False)]}),
1466         'move_ids': fields.many2many('stock.move', 'stock_inventory_move_rel', 'inventory_id', 'move_id', 'Created Moves'),
1467         'state': fields.selection( (('draft', 'Draft'), ('done', 'Done'), ('cancel','Cancelled')), 'State', readonly=True),
1468         'company_id': fields.many2one('res.company','Company',required=True,select=1),
1469     }
1470     _defaults = {
1471         'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
1472         'state': lambda *a: 'draft',
1473         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.inventory', context=c)
1474     }
1475
1476     #
1477     # Update to support tracking
1478     #
1479     def action_done(self, cr, uid, ids, context=None):
1480         for inv in self.browse(cr, uid, ids):
1481             move_ids = []
1482             move_line = []
1483             for line in inv.inventory_line_id:
1484                 pid = line.product_id.id
1485                 price = line.product_id.standard_price or 0.0
1486                 amount = self.pool.get('stock.location')._product_get(cr, uid, line.location_id.id, [pid], {'uom': line.product_uom.id})[pid]
1487                 change = line.product_qty - amount
1488                 if change:
1489                     location_id = line.product_id.product_tmpl_id.property_stock_inventory.id
1490                     value = {
1491                         'name': 'INV:' + str(line.inventory_id.id) + ':' + line.inventory_id.name,
1492                         'product_id': line.product_id.id,
1493                         'product_uom': line.product_uom.id,
1494                         'date': inv.date,
1495                         'date_planned': inv.date,
1496                         'state': 'assigned'
1497                     }
1498                     if change > 0:
1499                         value.update( {
1500                             'product_qty': change,
1501                             'location_id': location_id,
1502                             'location_dest_id': line.location_id.id,
1503                         })
1504                     else:
1505                         value.update( {
1506                             'product_qty': -change,
1507                             'location_id': line.location_id.id,
1508                             'location_dest_id': location_id,
1509                         })
1510                     move_ids.append(self.pool.get('stock.move').create(cr, uid, value))
1511             if len(move_ids):
1512                 self.pool.get('stock.move').action_done(cr, uid, move_ids,
1513                         context=context)
1514             self.write(cr, uid, [inv.id], {'state': 'done', 'date_done': time.strftime('%Y-%m-%d %H:%M:%S'), 'move_ids': [(6, 0, move_ids)]})
1515         return True
1516
1517     def action_cancel(self, cr, uid, ids, context={}):
1518         for inv in self.browse(cr, uid, ids):
1519             self.pool.get('stock.move').action_cancel(cr, uid, [x.id for x in inv.move_ids], context)
1520             self.write(cr, uid, [inv.id], {'state': 'draft'})
1521         return True
1522
1523     def action_cancel_inventary(self, cr, uid, ids, context={}):
1524         for inv in self.browse(cr,uid,ids):
1525             self.pool.get('stock.move').action_cancel(cr, uid, [x.id for x in inv.move_ids], context)
1526             self.write(cr, uid, [inv.id], {'state':'cancel'})
1527         return True
1528
1529 stock_inventory()
1530
1531
1532 class stock_inventory_line(osv.osv):
1533     _name = "stock.inventory.line"
1534     _description = "Inventory line"
1535     _columns = {
1536         'inventory_id': fields.many2one('stock.inventory', 'Inventory', ondelete='cascade', select=True),
1537         'location_id': fields.many2one('stock.location', 'Location', required=True),
1538         'product_id': fields.many2one('product.product', 'Product', required=True),
1539         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
1540         'product_qty': fields.float('Quantity'),
1541         'company_id': fields.related('inventory_id','company_id',type='many2one',relation='res.company',string='Company',store=True)
1542     }
1543
1544     def on_change_product_id(self, cr, uid, ids, location_id, product, uom=False):
1545         if not product:
1546             return {}
1547         if not uom:
1548             prod = self.pool.get('product.product').browse(cr, uid, [product], {'uom': uom})[0]
1549             uom = prod.uom_id.id
1550         amount = self.pool.get('stock.location')._product_get(cr, uid, location_id, [product], {'uom': uom})[product]
1551         result = {'product_qty': amount, 'product_uom': uom}
1552         return {'value': result}
1553
1554 stock_inventory_line()
1555
1556
1557 #----------------------------------------------------------
1558 # Stock Warehouse
1559 #----------------------------------------------------------
1560 class stock_warehouse(osv.osv):
1561     _name = "stock.warehouse"
1562     _description = "Warehouse"
1563     _columns = {
1564         'name': fields.char('Name', size=60, required=True),
1565 #       'partner_id': fields.many2one('res.partner', 'Owner'),
1566         'company_id': fields.many2one('res.company','Company',required=True,select=1),
1567         'partner_address_id': fields.many2one('res.partner.address', 'Owner Address'),
1568         'lot_input_id': fields.many2one('stock.location', 'Location Input', required=True),
1569         'lot_stock_id': fields.many2one('stock.location', 'Location Stock', required=True),
1570         'lot_output_id': fields.many2one('stock.location', 'Location Output', required=True),
1571     }
1572     _defaults = {
1573         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.inventory', context=c),
1574     }
1575 stock_warehouse()
1576
1577
1578 # Move wizard :
1579 #    get confirm or assign stock move lines of partner and put in current picking.
1580 class stock_picking_move_wizard(osv.osv_memory):
1581     _name = 'stock.picking.move.wizard'
1582
1583     def _get_picking(self, cr, uid, ctx):
1584         if ctx.get('action_id', False):
1585             return ctx['action_id']
1586         return False
1587
1588     def _get_picking_address(self, cr, uid, ctx):
1589         picking_obj = self.pool.get('stock.picking')
1590         if ctx.get('action_id', False):
1591             picking = picking_obj.browse(cr, uid, [ctx['action_id']])[0]
1592             return picking.address_id and picking.address_id.id or False
1593         return False
1594
1595     _columns = {
1596         'name': fields.char('Name', size=64, invisible=True),
1597         #'move_lines': fields.one2many('stock.move', 'picking_id', 'Move lines',readonly=True),
1598         'move_ids': fields.many2many('stock.move', 'picking_move_wizard_rel', 'picking_move_wizard_id', 'move_id', 'Entry lines', required=True),
1599         'address_id': fields.many2one('res.partner.address', 'Dest. Address', invisible=True),
1600         'picking_id': fields.many2one('stock.picking', 'Picking list', select=True, invisible=True),
1601     }
1602     _defaults = {
1603         'picking_id': _get_picking,
1604         'address_id': _get_picking_address,
1605     }
1606
1607     def action_move(self, cr, uid, ids, context=None):
1608         move_obj = self.pool.get('stock.move')
1609         picking_obj = self.pool.get('stock.picking')
1610         for act in self.read(cr, uid, ids):
1611             move_lines = move_obj.browse(cr, uid, act['move_ids'])
1612             for line in move_lines:
1613                 if line.picking_id:
1614                     picking_obj.write(cr, uid, [line.picking_id.id], {'move_lines': [(1, line.id, {'picking_id': act['picking_id']})]})
1615                     picking_obj.write(cr, uid, [act['picking_id']], {'move_lines': [(1, line.id, {'picking_id': act['picking_id']})]})
1616                     cr.commit()
1617                     old_picking = picking_obj.read(cr, uid, [line.picking_id.id])[0]
1618                     if not len(old_picking['move_lines']):
1619                         picking_obj.write(cr, uid, [old_picking['id']], {'state': 'done'})
1620                 else:
1621                     raise osv.except_osv(_('UserError'),
1622                         _('You can not create new moves.'))
1623         return {'type': 'ir.actions.act_window_close'}
1624
1625 stock_picking_move_wizard()
1626
1627 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: