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