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