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