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