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