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