[REF] stock: Task ID-575:Replaced scraped field on a stock.move by a related field...
[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         'scraped': fields.boolean('Scraped'),
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         'scraped': 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','scraped',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         'date_planned': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
1256         'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
1257         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.move', context=c)
1258     }
1259
1260     def copy(self, cr, uid, id, default=None, context={}):
1261         if default is None:
1262             default = {}
1263         default = default.copy()
1264         default['move_stock_return_history'] = []
1265         return super(stock_move, self).copy(cr, uid, id, default, context)
1266
1267     def create(self, cr, user, vals, context=None):
1268         if vals.get('move_stock_return_history',False):
1269             vals['move_stock_return_history'] = []
1270         return super(stock_move, self).create(cr, user, vals, context)
1271
1272     def _auto_init(self, cursor, context):
1273         res = super(stock_move, self)._auto_init(cursor, context)
1274         cursor.execute('SELECT indexname \
1275                 FROM pg_indexes \
1276                 WHERE indexname = \'stock_move_location_id_location_dest_id_product_id_state\'')
1277         if not cursor.fetchone():
1278             cursor.execute('CREATE INDEX stock_move_location_id_location_dest_id_product_id_state \
1279                     ON stock_move (location_id, location_dest_id, product_id, state)')
1280             cursor.commit()
1281         return res
1282
1283     def onchange_lot_id(self, cr, uid, ids, prodlot_id=False, product_qty=False, loc_id=False, product_id=False, context=None):
1284         if not prodlot_id or not loc_id:
1285             return {}
1286         ctx = context and context.copy() or {}
1287         ctx['location_id'] = loc_id
1288         prodlot = self.pool.get('stock.production.lot').browse(cr, uid, prodlot_id, ctx)
1289         location = self.pool.get('stock.location').browse(cr, uid, loc_id)
1290         warning = {}
1291         if (location.usage == 'internal') and (product_qty > (prodlot.stock_available or 0.0)):
1292             warning = {
1293                 'title': 'Bad Lot Assignation !',
1294                 'message': 'You are moving %.2f products but only %.2f available in this lot.' % (product_qty, prodlot.stock_available or 0.0)
1295             }
1296         return {'warning': warning}
1297
1298     def onchange_quantity(self, cr, uid, ids, product_id, product_qty, product_uom, product_uos):
1299         result = {
1300                   'product_uos_qty': 0.00
1301           }
1302
1303         if (not product_id) or (product_qty <=0.0):
1304             return {'value': result}
1305
1306         product_obj = self.pool.get('product.product')
1307         uos_coeff = product_obj.read(cr, uid, product_id, ['uos_coeff'])
1308
1309         if product_uos and product_uom and (product_uom != product_uos):
1310             result['product_uos_qty'] = product_qty * uos_coeff['uos_coeff']
1311         else:
1312             result['product_uos_qty'] = product_qty
1313
1314         return {'value': result}
1315
1316     def onchange_product_id(self, cr, uid, ids, prod_id=False, loc_id=False, loc_dest_id=False, address_id=False):
1317         if not prod_id:
1318             return {}
1319         lang = False
1320         if address_id:
1321             addr_rec = self.pool.get('res.partner.address').browse(cr, uid, address_id)
1322             if addr_rec:
1323                 lang = addr_rec.partner_id and addr_rec.partner_id.lang or False
1324         ctx = {'lang': lang}
1325
1326         product = self.pool.get('product.product').browse(cr, uid, [prod_id], context=ctx)[0]
1327         uos_id  = product.uos_id and product.uos_id.id or False
1328         result = {
1329             'product_uom': product.uom_id.id,
1330             'product_uos': uos_id,
1331             'product_qty': 1.00,
1332             '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']
1333         }
1334         if not ids:
1335             result['name'] = product.partner_ref
1336         if loc_id:
1337             result['location_id'] = loc_id
1338         if loc_dest_id:
1339             result['location_dest_id'] = loc_dest_id
1340         return {'value': result}
1341
1342     def _chain_compute(self, cr, uid, moves, context={}):
1343         result = {}
1344         for m in moves:
1345             dest = self.pool.get('stock.location').chained_location_get(
1346                 cr,
1347                 uid,
1348                 m.location_dest_id,
1349                 m.picking_id and m.picking_id.address_id and m.picking_id.address_id.partner_id,
1350                 m.product_id,
1351                 context
1352             )
1353             if dest:
1354                 if dest[1] == 'transparent':
1355                     self.write(cr, uid, [m.id], {
1356                         'date_planned': (datetime.strptime(m.date_planned, '%Y-%m-%d %H:%M:%S') + \
1357                             relativedelta(days=dest[2] or 0)).strftime('%Y-%m-%d'),
1358                         'location_dest_id': dest[0].id})
1359                 else:
1360                     result.setdefault(m.picking_id, [])
1361                     result[m.picking_id].append( (m, dest) )
1362         return result
1363
1364     def action_confirm(self, cr, uid, ids, context={}):
1365 #        ids = map(lambda m: m.id, moves)
1366         moves = self.browse(cr, uid, ids)
1367         self.write(cr, uid, ids, {'state': 'confirmed'})
1368         i = 0
1369
1370         def create_chained_picking(self, cr, uid, moves, context):
1371             new_moves = []
1372             for picking, todo in self._chain_compute(cr, uid, moves, context).items():
1373                 ptype = self.pool.get('stock.location').picking_type_get(cr, uid, todo[0][0].location_dest_id, todo[0][1][0])
1374                 pick_name = ''
1375                 if ptype == 'delivery':
1376                     pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.delivery')
1377                 pickid = self.pool.get('stock.picking').create(cr, uid, {
1378                     'name': pick_name or picking.name,
1379                     'origin': str(picking.origin or ''),
1380                     'type': ptype,
1381                     'note': picking.note,
1382                     'move_type': picking.move_type,
1383                     'auto_picking': todo[0][1][1] == 'auto',
1384                     'address_id': picking.address_id.id,
1385                     'invoice_state': 'none'
1386                 })
1387                 for move, (loc, auto, delay) in todo:
1388                     # Is it smart to copy ? May be it's better to recreate ?
1389                     new_id = self.pool.get('stock.move').copy(cr, uid, move.id, {
1390                         'location_id': move.location_dest_id.id,
1391                         'location_dest_id': loc.id,
1392                         'date_moved': time.strftime('%Y-%m-%d'),
1393                         'picking_id': pickid,
1394                         'state': 'waiting',
1395                         'move_history_ids': [],
1396                         'date_planned': (datetime.strptime(move.date_planned, '%Y-%m-%d %H:%M:%S') + relativedelta(days=delay or 0)).strftime('%Y-%m-%d'),
1397                         'move_history_ids2': []}
1398                     )
1399                     self.pool.get('stock.move').write(cr, uid, [move.id], {
1400                         'move_dest_id': new_id,
1401                         'move_history_ids': [(4, new_id)]
1402                     })
1403                     new_moves.append(self.browse(cr, uid, [new_id])[0])
1404                 wf_service = netsvc.LocalService("workflow")
1405                 wf_service.trg_validate(uid, 'stock.picking', pickid, 'button_confirm', cr)
1406             if new_moves:
1407                 create_chained_picking(self, cr, uid, new_moves, context)
1408         create_chained_picking(self, cr, uid, moves, context)
1409         return []
1410
1411     def action_assign(self, cr, uid, ids, *args):
1412         todo = []
1413         for move in self.browse(cr, uid, ids):
1414             if move.state in ('confirmed', 'waiting'):
1415                 todo.append(move.id)
1416         res = self.check_assign(cr, uid, todo)
1417         return res
1418
1419     def force_assign(self, cr, uid, ids, context={}):
1420         self.write(cr, uid, ids, {'state': 'assigned'})
1421         return True
1422
1423     def cancel_assign(self, cr, uid, ids, context={}):
1424         self.write(cr, uid, ids, {'state': 'confirmed'})
1425         return True
1426
1427     #
1428     # Duplicate stock.move
1429     #
1430     def check_assign(self, cr, uid, ids, context={}):
1431         done = []
1432         count = 0
1433         pickings = {}
1434         for move in self.browse(cr, uid, ids):
1435             if move.product_id.type == 'consu':
1436                 if move.state in ('confirmed', 'waiting'):
1437                     done.append(move.id)
1438                 pickings[move.picking_id.id] = 1
1439                 continue
1440             if move.state in ('confirmed', 'waiting'):
1441                 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})
1442                 if res:
1443                     #_product_available_test depends on the next status for correct functioning
1444                     #the test does not work correctly if the same product occurs multiple times
1445                     #in the same order. This is e.g. the case when using the button 'split in two' of
1446                     #the stock outgoing form
1447                     self.write(cr, uid, move.id, {'state':'assigned'})
1448                     done.append(move.id)
1449                     pickings[move.picking_id.id] = 1
1450                     r = res.pop(0)
1451                     cr.execute('update stock_move set location_id=%s, product_qty=%s where id=%s', (r[1], r[0], move.id))
1452
1453                     while res:
1454                         r = res.pop(0)
1455                         move_id = self.copy(cr, uid, move.id, {'product_qty': r[0], 'location_id': r[1]})
1456                         done.append(move_id)
1457                         #cr.execute('insert into stock_move_history_ids values (%s,%s)', (move.id,move_id))
1458         if done:
1459             count += len(done)
1460             self.write(cr, uid, done, {'state': 'assigned'})
1461
1462         if count:
1463             for pick_id in pickings:
1464                 wf_service = netsvc.LocalService("workflow")
1465                 wf_service.trg_write(uid, 'stock.picking', pick_id, cr)
1466         return count
1467
1468     #
1469     # Cancel move => cancel others move and pickings
1470     #
1471     def action_cancel(self, cr, uid, ids, context={}):
1472         if not len(ids):
1473             return True
1474         pickings = {}
1475         for move in self.browse(cr, uid, ids):
1476             if move.state in ('confirmed', 'waiting', 'assigned', 'draft'):
1477                 if move.picking_id:
1478                     pickings[move.picking_id.id] = True
1479             if move.move_dest_id and move.move_dest_id.state == 'waiting':
1480                 self.write(cr, uid, [move.move_dest_id.id], {'state': 'assigned'})
1481                 if context.get('call_unlink',False) and move.move_dest_id.picking_id:
1482                     wf_service = netsvc.LocalService("workflow")
1483                     wf_service.trg_write(uid, 'stock.picking', move.move_dest_id.picking_id.id, cr)
1484         self.write(cr, uid, ids, {'state': 'cancel', 'move_dest_id': False})
1485         if not context.get('call_unlink',False):
1486             for pick in self.pool.get('stock.picking').browse(cr, uid, pickings.keys()):
1487                 if all(move.state == 'cancel' for move in pick.move_lines):
1488                     self.pool.get('stock.picking').write(cr, uid, [pick.id], {'state': 'cancel'})
1489
1490         wf_service = netsvc.LocalService("workflow")
1491         for id in ids:
1492             wf_service.trg_trigger(uid, 'stock.move', id, cr)
1493         #self.action_cancel(cr,uid, ids2, context)
1494         return True
1495
1496     def action_done(self, cr, uid, ids, context=None):
1497         track_flag = False
1498         picking_ids = []
1499         for move in self.browse(cr, uid, ids):
1500             if move.picking_id: picking_ids.append(move.picking_id.id)
1501             if move.move_dest_id.id and (move.state != 'done'):
1502                 cr.execute('insert into stock_move_history_ids (parent_id,child_id) values (%s,%s)', (move.id, move.move_dest_id.id))
1503                 if move.move_dest_id.state in ('waiting', 'confirmed'):
1504                     self.write(cr, uid, [move.move_dest_id.id], {'state': 'assigned'})
1505                     if move.move_dest_id.picking_id:
1506                         wf_service = netsvc.LocalService("workflow")
1507                         wf_service.trg_write(uid, 'stock.picking', move.move_dest_id.picking_id.id, cr)
1508                     else:
1509                         pass
1510                         # self.action_done(cr, uid, [move.move_dest_id.id])
1511                     if move.move_dest_id.auto_validate:
1512                         self.action_done(cr, uid, [move.move_dest_id.id], context=context)
1513
1514             #
1515             # Accounting Entries
1516             #
1517             acc_src = None
1518             acc_dest = None
1519             if move.location_id.account_id:
1520                 acc_src = move.location_id.account_id.id
1521             if move.location_dest_id.account_id:
1522                 acc_dest = move.location_dest_id.account_id.id
1523             if acc_src or acc_dest:
1524                 test = [('product.product', move.product_id.id)]
1525                 if move.product_id.categ_id:
1526                     test.append( ('product.category', move.product_id.categ_id.id) )
1527                 if not acc_src:
1528                     acc_src = move.product_id.product_tmpl_id.\
1529                             property_stock_account_input.id
1530                     if not acc_src:
1531                         acc_src = move.product_id.categ_id.\
1532                                 property_stock_account_input_categ.id
1533                     if not acc_src:
1534                         raise osv.except_osv(_('Error!'),
1535                                 _('There is no stock input account defined ' \
1536                                         'for this product: "%s" (id: %d)') % \
1537                                         (move.product_id.name,
1538                                             move.product_id.id,))
1539                 if not acc_dest:
1540                     acc_dest = move.product_id.product_tmpl_id.\
1541                             property_stock_account_output.id
1542                     if not acc_dest:
1543                         acc_dest = move.product_id.categ_id.\
1544                                 property_stock_account_output_categ.id
1545                     if not acc_dest:
1546                         raise osv.except_osv(_('Error!'),
1547                                 _('There is no stock output account defined ' \
1548                                         'for this product: "%s" (id: %d)') % \
1549                                         (move.product_id.name,
1550                                             move.product_id.id,))
1551                 if not move.product_id.categ_id.property_stock_journal.id:
1552                     raise osv.except_osv(_('Error!'),
1553                         _('There is no journal defined '\
1554                             'on the product category: "%s" (id: %d)') % \
1555                             (move.product_id.categ_id.name,
1556                                 move.product_id.categ_id.id,))
1557                 journal_id = move.product_id.categ_id.property_stock_journal.id
1558                 if acc_src != acc_dest:
1559                     ref = move.picking_id and move.picking_id.name or False
1560                     product_uom_obj = self.pool.get('product.uom')
1561                     default_uom = move.product_id.uom_id.id
1562                     date = time.strftime('%Y-%m-%d')
1563                     q = product_uom_obj._compute_qty(cr, uid, move.product_uom.id, move.product_qty, default_uom)
1564                     if move.product_id.cost_method == 'average' and move.price_unit:
1565                         amount = q * move.price_unit
1566                     # Base computation on valuation price type
1567                     else:
1568                         company_id=move.company_id.id
1569                         context['currency_id']=move.company_id.currency_id.id
1570                         pricetype=self.pool.get('product.price.type').browse(cr,uid,move.company_id.property_valuation_price_type.id)
1571                         amount_unit=move.product_id.price_get(pricetype.field, context)[move.product_id.id]
1572                         amount=amount_unit * q or 1.0
1573                         # amount = q * move.product_id.standard_price
1574
1575                     partner_id = False
1576                     if move.picking_id:
1577                         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
1578                     lines = [
1579                             (0, 0, {
1580                                 'name': move.name,
1581                                 'quantity': move.product_qty,
1582                                 'product_id': move.product_id and move.product_id.id or False,
1583                                 'credit': amount,
1584                                 'account_id': acc_src,
1585                                 'ref': ref,
1586                                 'date': date,
1587                                 'partner_id': partner_id}),
1588                             (0, 0, {
1589                                 'name': move.name,
1590                                 'product_id': move.product_id and move.product_id.id or False,
1591                                 'quantity': move.product_qty,
1592                                 'debit': amount,
1593                                 'account_id': acc_dest,
1594                                 'ref': ref,
1595                                 'date': date,
1596                                 'partner_id': partner_id})
1597                     ]
1598                     self.pool.get('account.move').create(cr, uid, {
1599                         'name': move.name,
1600                         'journal_id': journal_id,
1601                         'line_id': lines,
1602                         'ref': ref,
1603                     })
1604         self.write(cr, uid, ids, {'state': 'done', 'date_planned': time.strftime('%Y-%m-%d %H:%M:%S')})
1605         for pick in self.pool.get('stock.picking').browse(cr, uid, picking_ids):
1606             if all(move.state == 'done' for move in pick.move_lines):
1607                 self.pool.get('stock.picking').action_done(cr, uid, [pick.id])
1608
1609         wf_service = netsvc.LocalService("workflow")
1610         for id in ids:
1611             wf_service.trg_trigger(uid, 'stock.move', id, cr)
1612         return True
1613
1614     def unlink(self, cr, uid, ids, context=None):
1615         for move in self.browse(cr, uid, ids, context=context):
1616             if move.state != 'draft':
1617                 raise osv.except_osv(_('UserError'),
1618                         _('You can only delete draft moves.'))
1619         return super(stock_move, self).unlink(
1620             cr, uid, ids, context=context)
1621
1622     def _create_lot(self, cr, uid, ids, product_id, prefix=False):
1623         prodlot_obj = self.pool.get('stock.production.lot')
1624         ir_sequence_obj = self.pool.get('ir.sequence')
1625         sequence = ir_sequence_obj.get(cr, uid, 'stock.lot.serial')
1626         if not sequence:
1627             raise osv.except_osv(_('Error!'), _('No production sequence defined'))
1628         prodlot_id = prodlot_obj.create(cr, uid, {'name': sequence, 'prefix': prefix}, {'product_id': product_id})
1629         prodlot = prodlot_obj.browse(cr, uid, prodlot_id)
1630         ref = ','.join(map(lambda x:str(x),ids))
1631         if prodlot.ref:
1632             ref = '%s, %s' % (prodlot.ref, ref)
1633         prodlot_obj.write(cr, uid, [prodlot_id], {'ref': ref})
1634         return prodlot_id
1635
1636
1637     def action_scrap(self, cr, uid, ids, quantity, location_id, context=None):
1638         '''
1639         Move the scrap/damaged product into scrap location
1640
1641         @ param cr: the database cursor
1642         @ param uid: the user id
1643         @ param ids: ids of stock move object to be scraped
1644         @ param quantity : specify scrap qty
1645         @ param location_id : specify scrap location
1646         @ param context: context arguments
1647
1648         @ return: Scraped lines
1649         '''
1650         if quantity <= 0:
1651             raise osv.except_osv(_('Warning!'), _('Please provide Proper Quantity !'))
1652         res = []
1653         for move in self.browse(cr, uid, ids, context=context):
1654             move_qty = move.product_qty
1655             uos_qty = quantity / move_qty * move.product_uos_qty
1656             default_val = {
1657                     'product_qty': quantity,
1658                     'product_uos_qty': uos_qty,
1659                     'state': move.state,
1660                     'scraped' : True,
1661                     'location_dest_id': location_id
1662                 }
1663             new_move = self.copy(cr, uid, move.id, default_val)
1664             #self.write(cr, uid, [new_move], {'move_history_ids':[(4,move.id)]}) #TODO : to track scrap moves
1665             res += [new_move]
1666         self.action_done(cr, uid, res)
1667         return res
1668
1669     def action_split(self, cr, uid, ids, quantity, split_by_qty=1, prefix=False, with_lot=True, context=None):
1670         '''
1671         Split Stock Move lines into production lot which specified split by quantity.
1672
1673         @ param cr: the database cursor
1674         @ param uid: the user id
1675         @ param ids: ids of stock move object to be splited
1676         @ param split_by_qty : specify split by qty
1677         @ param prefix : specify prefix of production lot
1678         @ param with_lot : if true, prodcution lot will assign for split line otherwise not.
1679         @ param context: context arguments
1680
1681         @ return: splited move lines
1682         '''
1683
1684         if quantity <= 0:
1685             raise osv.except_osv(_('Warning!'), _('Please provide Proper Quantity !'))
1686
1687         res = []
1688
1689         for move in self.browse(cr, uid, ids):
1690             if split_by_qty <= 0 or quantity == 0:
1691                 return res
1692
1693             uos_qty = split_by_qty / move.product_qty * move.product_uos_qty
1694
1695             quantity_rest = quantity % split_by_qty
1696             uos_qty_rest = split_by_qty / move.product_qty * move.product_uos_qty
1697
1698             update_val = {
1699                 'product_qty': split_by_qty,
1700                 'product_uos_qty': uos_qty,
1701             }
1702             for idx in range(int(quantity//split_by_qty)):
1703                 if not idx and move.product_qty<=quantity:
1704                     current_move = move.id
1705                 else:
1706                     current_move = self.copy(cr, uid, move.id, {'state': move.state})
1707                 res.append(current_move)
1708                 if with_lot:
1709                     update_val['prodlot_id'] = self._create_lot(cr, uid, [current_move], move.product_id.id)
1710
1711                 self.write(cr, uid, [current_move], update_val)
1712
1713
1714             if quantity_rest > 0:
1715                 idx = int(quantity//split_by_qty)
1716                 update_val['product_qty'] = quantity_rest
1717                 update_val['product_uos_qty'] = uos_qty_rest
1718                 if not idx and move.product_qty<=quantity:
1719                     current_move = move.id
1720                 else:
1721                     current_move = self.copy(cr, uid, move.id, {'state': move.state})
1722
1723                 res.append(current_move)
1724
1725
1726                 if with_lot:
1727                     update_val['prodlot_id'] = self._create_lot(cr, uid, [current_move], move.product_id.id)
1728
1729                 self.write(cr, uid, [current_move], update_val)
1730         return res
1731
1732     def action_consume(self, cr, uid, ids, quantity, location_id=False,  context=None):
1733         '''
1734         Consumed product with specific quatity from specific source location
1735
1736         @ param cr: the database cursor
1737         @ param uid: the user id
1738         @ param ids: ids of stock move object to be consumed
1739         @ param quantity : specify consume quantity
1740         @ param location_id : specify source location
1741         @ param context: context arguments
1742
1743         @ return: Consumed lines
1744         '''
1745         if not context:
1746             context = {}
1747
1748         if quantity <= 0:
1749             raise osv.except_osv(_('Warning!'), _('Please provide Proper Quantity !'))
1750
1751         res = []
1752         for move in self.browse(cr, uid, ids, context=context):
1753             move_qty = move.product_qty
1754             quantity_rest = move.product_qty
1755
1756             quantity_rest -= quantity
1757             uos_qty_rest = quantity_rest / move_qty * move.product_uos_qty
1758             if quantity_rest <= 0:
1759                 quantity_rest = 0
1760                 uos_qty_rest = 0
1761                 quantity = move.product_qty
1762
1763             uos_qty = quantity / move_qty * move.product_uos_qty
1764
1765             if quantity_rest > 0:
1766                 default_val = {
1767                     'product_qty': quantity,
1768                     'product_uos_qty': uos_qty,
1769                     'state': move.state,
1770                     'location_id': location_id
1771                 }
1772                 if move.product_id.track_production and location_id:
1773                     # IF product has checked track for production lot, move lines will be split by 1
1774                     res += self.action_split(cr, uid, [move.id], quantity, split_by_qty=1, context=context)
1775                 else:
1776                     current_move = self.copy(cr, uid, move.id, default_val)
1777                     res += [current_move]
1778
1779                 update_val = {}
1780                 update_val['product_qty'] = quantity_rest
1781                 update_val['product_uos_qty'] = uos_qty_rest
1782                 self.write(cr, uid, [move.id], update_val)
1783
1784             else:
1785                 quantity_rest = quantity
1786                 uos_qty_rest =  uos_qty
1787
1788                 if move.product_id.track_production and location_id:
1789                     res += self.split_lines(cr, uid, [move.id], quantity_rest, split_by_qty=1, context=context)
1790                 else:
1791                     res += [move.id]
1792                     update_val = {
1793                         'product_qty' : quantity_rest,
1794                         'product_uos_qty' : uos_qty_rest,
1795                         'location_id': location_id
1796                     }
1797
1798                     self.write(cr, uid, [move.id], update_val)
1799
1800         self.action_done(cr, uid, res)
1801         return res
1802
1803     def do_partial(self, cr, uid, ids, partial_datas, context={}):
1804         """
1805         @ partial_datas : dict. contain details of partial picking
1806                           like partner_id, address_id, delivery_date, delivery moves with product_id, product_qty, uom
1807         """
1808         res = {}
1809         picking_obj = self.pool.get('stock.picking')
1810         delivery_obj = self.pool.get('stock.delivery')
1811         product_obj = self.pool.get('product.product')
1812         currency_obj = self.pool.get('res.currency')
1813         users_obj = self.pool.get('res.users')
1814         uom_obj = self.pool.get('product.uom')
1815         price_type_obj = self.pool.get('product.price.type')
1816         sequence_obj = self.pool.get('ir.sequence')
1817         wf_service = netsvc.LocalService("workflow")
1818         partner_id = partial_datas.get('partner_id', False)
1819         address_id = partial_datas.get('address_id', False)
1820         delivery_date = partial_datas.get('delivery_date', False)
1821
1822         new_moves = []
1823
1824         complete, too_many, too_few = [], [], []
1825         move_product_qty = {}
1826         for move in self.browse(cr, uid, ids, context=context):
1827             if move.state in ('done', 'cancel'):
1828                 continue
1829             partial_data = partial_datas.get('move%s'%(move.id), False)
1830             assert partial_data, _('Do not Found Partial data of Stock Move Line :%s' %(move.id))
1831             product_qty = partial_data.get('product_qty',0.0)
1832             move_product_qty[move.id] = product_qty
1833             product_uom = partial_data.get('product_uom',False)
1834             product_price = partial_data.get('product_price',0.0)
1835             product_currency = partial_data.get('product_currency',False)
1836             if move.product_qty == product_qty:
1837                 complete.append(move)
1838             elif move.product_qty > product_qty:
1839                 too_few.append(move)
1840             else:
1841                 too_many.append(move)
1842
1843             # Average price computation
1844             if (move.picking_id.type == 'in') and (move.product_id.cost_method == 'average'):
1845                 product = product_obj.browse(cr, uid, move.product_id.id)
1846                 user = users_obj.browse(cr, uid, uid)
1847                 context['currency_id'] = move.company_id.currency_id.id
1848                 qty = uom_obj._compute_qty(cr, uid, product_uom, product_qty, product.uom_id.id)
1849                 pricetype = False
1850                 if user.company_id.property_valuation_price_type:
1851                     pricetype = price_type_obj.browse(cr, uid, user.company_id.property_valuation_price_type.id)
1852                 if pricetype and qty > 0:
1853                     new_price = currency_obj.compute(cr, uid, product_currency,
1854                             user.company_id.currency_id.id, product_price)
1855                     new_price = uom_obj._compute_price(cr, uid, product_uom, new_price,
1856                             product.uom_id.id)
1857                     if product.qty_available <= 0:
1858                         new_std_price = new_price
1859                     else:
1860                         # Get the standard price
1861                         amount_unit = product.price_get(pricetype.field, context)[product.id]
1862                         new_std_price = ((amount_unit * product.qty_available)\
1863                             + (new_price * qty))/(product.qty_available + qty)
1864
1865                     # Write the field according to price type field
1866                     product_obj.write(cr, uid, [product.id],
1867                             {pricetype.field: new_std_price})
1868                     self.write(cr, uid, [move.id], {'price_unit': new_price})
1869
1870         for move in too_few:
1871             product_qty = move_product_qty[move.id]
1872             if product_qty != 0:
1873                 new_move = self.copy(cr, uid, move.id,
1874                     {
1875                         'product_qty' : product_qty,
1876                         'product_uos_qty': product_qty,
1877                         'picking_id' : move.picking_id.id,
1878                         'state': 'assigned',
1879                         'move_dest_id': False,
1880                         'price_unit': move.price_unit,
1881                     })
1882                 complete.append(self.browse(cr, uid, new_move))
1883             self.write(cr, uid, move.id,
1884                     {
1885                         'product_qty' : move.product_qty - product_qty,
1886                         'product_uos_qty':move.product_qty - product_qty,
1887                     })
1888
1889
1890         for move in too_many:
1891             self.write(cr, uid, move.id,
1892                     {
1893                         'product_qty': product_qty,
1894                         'product_uos_qty': product_qty
1895                     })
1896             complete.append(move)
1897
1898         for move in complete:
1899             self.action_done(cr, uid, [move.id])
1900
1901             # TOCHECK : Done picking if all moves are done
1902             cr.execute("""
1903                 SELECT move.id FROM stock_picking pick
1904                 RIGHT JOIN stock_move move ON move.picking_id = pick.id AND move.state = %s
1905                 WHERE pick.id = %s""",
1906                         ('done', move.picking_id.id))
1907             res = cr.fetchall()
1908             if len(res) == len(move.picking_id.move_lines):
1909                 picking_obj.action_move(cr, uid, [move.picking_id.id])
1910                 wf_service.trg_validate(uid, 'stock.picking', move.picking_id.id, 'button_done', cr)
1911
1912         ref = {}
1913         done_move_ids = []
1914         for move in complete:
1915             done_move_ids.append(move.id)
1916             if move.picking_id.id not in ref:
1917                 delivery_id = delivery_obj.create(cr, uid, {
1918                     'partner_id': partner_id,
1919                     'address_id': address_id,
1920                     'date': delivery_date,
1921                     'name' : move.picking_id.name,
1922                     'picking_id':  move.picking_id.id
1923                 }, context=context)
1924                 ref[move.picking_id.id] = delivery_id
1925             delivery_obj.write(cr, uid, ref[move.picking_id.id], {
1926                 'move_delivered' : [(4,move.id)]
1927             })
1928         return done_move_ids
1929
1930 stock_move()
1931
1932
1933 class stock_inventory(osv.osv):
1934     _name = "stock.inventory"
1935     _description = "Inventory"
1936     _columns = {
1937         'name': fields.char('Inventory', size=64, required=True, readonly=True, states={'draft': [('readonly', False)]}),
1938         'date': fields.datetime('Date create', required=True, readonly=True, states={'draft': [('readonly', False)]}),
1939         'date_done': fields.datetime('Date done'),
1940         'inventory_line_id': fields.one2many('stock.inventory.line', 'inventory_id', 'Inventories', states={'done': [('readonly', True)]}),
1941         'move_ids': fields.many2many('stock.move', 'stock_inventory_move_rel', 'inventory_id', 'move_id', 'Created Moves'),
1942         'state': fields.selection( (('draft', 'Draft'), ('done', 'Done'), ('cancel','Cancelled')), 'State', readonly=True),
1943         'company_id': fields.many2one('res.company','Company',required=True,select=1),
1944     }
1945     _defaults = {
1946         'date': lambda *a: time.strftime('%Y-%m-%d %H:%M:%S'),
1947         'state': lambda *a: 'draft',
1948         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.inventory', context=c)
1949     }
1950
1951
1952     def _inventory_line_hook(self, cr, uid, inventory_line, move_vals):
1953         '''Creates a stock move from an inventory line'''
1954         return self.pool.get('stock.move').create(cr, uid, move_vals)
1955
1956     def action_done(self, cr, uid, ids, context=None):
1957         for inv in self.browse(cr, uid, ids):
1958             move_ids = []
1959             move_line = []
1960             for line in inv.inventory_line_id:
1961                 pid = line.product_id.id
1962
1963                 # price = line.product_id.standard_price or 0.0
1964                 amount = self.pool.get('stock.location')._product_get(cr, uid, line.location_id.id, [pid], {'uom': line.product_uom.id})[pid]
1965                 change = line.product_qty - amount
1966                 lot_id = line.prod_lot_id.id
1967                 if change:
1968                     location_id = line.product_id.product_tmpl_id.property_stock_inventory.id
1969                     value = {
1970                         'name': 'INV:' + str(line.inventory_id.id) + ':' + line.inventory_id.name,
1971                         'product_id': line.product_id.id,
1972                         'product_uom': line.product_uom.id,
1973                         'prodlot_id': lot_id,
1974                         'date': inv.date,
1975                         'date_planned': inv.date,
1976                         'state': 'assigned'
1977                     }
1978                     if change > 0:
1979                         value.update( {
1980                             'product_qty': change,
1981                             'location_id': location_id,
1982                             'location_dest_id': line.location_id.id,
1983                         })
1984                     else:
1985                         value.update( {
1986                             'product_qty': -change,
1987                             'location_id': line.location_id.id,
1988                             'location_dest_id': location_id,
1989                         })
1990                     if lot_id:
1991                         value.update({
1992                             'prodlot_id': lot_id,
1993                             'product_qty': line.product_qty
1994                         })
1995                     move_ids.append(self._inventory_line_hook(cr, uid, line, value))
1996             if len(move_ids):
1997                 self.pool.get('stock.move').action_done(cr, uid, move_ids,
1998                         context=context)
1999             self.write(cr, uid, [inv.id], {'state': 'done', 'date_done': time.strftime('%Y-%m-%d %H:%M:%S'), 'move_ids': [(6, 0, move_ids)]})
2000         return True
2001
2002     def action_cancel(self, cr, uid, ids, context={}):
2003         for inv in self.browse(cr, uid, ids):
2004             self.pool.get('stock.move').action_cancel(cr, uid, [x.id for x in inv.move_ids], context)
2005             self.write(cr, uid, [inv.id], {'state': 'draft'})
2006         return True
2007
2008     def action_cancel_inventary(self, cr, uid, ids, context={}):
2009         for inv in self.browse(cr,uid,ids):
2010             self.pool.get('stock.move').action_cancel(cr, uid, [x.id for x in inv.move_ids], context)
2011             self.write(cr, uid, [inv.id], {'state':'cancel'})
2012         return True
2013
2014 stock_inventory()
2015
2016
2017 class stock_inventory_line(osv.osv):
2018     _name = "stock.inventory.line"
2019     _description = "Inventory line"
2020     _columns = {
2021         'inventory_id': fields.many2one('stock.inventory', 'Inventory', ondelete='cascade', select=True),
2022         'location_id': fields.many2one('stock.location', 'Location', required=True),
2023         'product_id': fields.many2one('product.product', 'Product', required=True),
2024         'product_uom': fields.many2one('product.uom', 'Product UOM', required=True),
2025         'product_qty': fields.float('Quantity'),
2026         'company_id': fields.related('inventory_id','company_id',type='many2one',relation='res.company',string='Company',store=True),
2027         'prod_lot_id': fields.many2one('stock.production.lot', 'Production Lot', domain="[('product_id','=',product_id)]"),
2028         'state': fields.related('inventory_id','state',type='char',string='State',readonly=True),
2029     }
2030
2031     def on_change_product_id(self, cr, uid, ids, location_id, product, uom=False):
2032         if not product:
2033             return {}
2034         if not uom:
2035             prod = self.pool.get('product.product').browse(cr, uid, [product], {'uom': uom})[0]
2036             uom = prod.uom_id.id
2037         amount = self.pool.get('stock.location')._product_get(cr, uid, location_id, [product], {'uom': uom})[product]
2038         result = {'product_qty': amount, 'product_uom': uom}
2039         return {'value': result}
2040
2041 stock_inventory_line()
2042
2043
2044 #----------------------------------------------------------
2045 # Stock Warehouse
2046 #----------------------------------------------------------
2047 class stock_warehouse(osv.osv):
2048     _name = "stock.warehouse"
2049     _description = "Warehouse"
2050     _columns = {
2051         'name': fields.char('Name', size=60, required=True),
2052 #       'partner_id': fields.many2one('res.partner', 'Owner'),
2053         'company_id': fields.many2one('res.company','Company',required=True,select=1),
2054         'partner_address_id': fields.many2one('res.partner.address', 'Owner Address'),
2055         'lot_input_id': fields.many2one('stock.location', 'Location Input', required=True),
2056         'lot_stock_id': fields.many2one('stock.location', 'Location Stock', required=True),
2057         'lot_output_id': fields.many2one('stock.location', 'Location Output', required=True),
2058     }
2059     _defaults = {
2060         'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.inventory', context=c),
2061     }
2062 stock_warehouse()
2063
2064
2065 # Move wizard :
2066 #    get confirm or assign stock move lines of partner and put in current picking.
2067 class stock_picking_move_wizard(osv.osv_memory):
2068     _name = 'stock.picking.move.wizard'
2069
2070     def _get_picking(self, cr, uid, ctx):
2071         if ctx.get('action_id', False):
2072             return ctx['action_id']
2073         return False
2074
2075     def _get_picking_address(self, cr, uid, ctx):
2076         picking_obj = self.pool.get('stock.picking')
2077         if ctx.get('action_id', False):
2078             picking = picking_obj.browse(cr, uid, [ctx['action_id']])[0]
2079             return picking.address_id and picking.address_id.id or False
2080         return False
2081
2082     _columns = {
2083         'name': fields.char('Name', size=64, invisible=True),
2084         #'move_lines': fields.one2many('stock.move', 'picking_id', 'Move lines',readonly=True),
2085         'move_ids': fields.many2many('stock.move', 'picking_move_wizard_rel', 'picking_move_wizard_id', 'move_id', 'Entry lines', required=True),
2086         'address_id': fields.many2one('res.partner.address', 'Dest. Address', invisible=True),
2087         'picking_id': fields.many2one('stock.picking', 'Picking list', select=True, invisible=True),
2088     }
2089     _defaults = {
2090         'picking_id': _get_picking,
2091         'address_id': _get_picking_address,
2092     }
2093
2094     def action_move(self, cr, uid, ids, context=None):
2095         move_obj = self.pool.get('stock.move')
2096         picking_obj = self.pool.get('stock.picking')
2097         for act in self.read(cr, uid, ids):
2098             move_lines = move_obj.browse(cr, uid, act['move_ids'])
2099             for line in move_lines:
2100                 if line.picking_id:
2101                     picking_obj.write(cr, uid, [line.picking_id.id], {'move_lines': [(1, line.id, {'picking_id': act['picking_id']})]})
2102                     picking_obj.write(cr, uid, [act['picking_id']], {'move_lines': [(1, line.id, {'picking_id': act['picking_id']})]})
2103                     old_picking = picking_obj.read(cr, uid, [line.picking_id.id])[0]
2104                     if not len(old_picking['move_lines']):
2105                         picking_obj.write(cr, uid, [old_picking['id']], {'state': 'done'})
2106                 else:
2107                     raise osv.except_osv(_('UserError'),
2108                         _('You can not create new moves.'))
2109         return {'type': 'ir.actions.act_window_close'}
2110
2111 stock_picking_move_wizard()
2112
2113 class report_products_to_received_planned(osv.osv):
2114     _name = "report.products.to.received.planned"
2115     _description = "Product to Received Vs Planned"
2116     _auto = False
2117     _columns = {
2118         'date':fields.date('Date'),
2119         'qty': fields.integer('Actual Qty'),
2120         'planned_qty': fields.integer('Planned Qty'),
2121
2122     }
2123
2124     def init(self, cr):
2125         tools.drop_view_if_exists(cr, 'report_products_to_received_planned')
2126         cr.execute("""
2127             create or replace view report_products_to_received_planned as (
2128                select stock.date, min(stock.id) as id, sum(stock.product_qty) as qty, 0 as planned_qty
2129                    from stock_picking picking
2130                     inner join stock_move stock
2131                     on picking.id = stock.picking_id and picking.type = 'in'
2132                     where stock.date between (select cast(date_trunc('week', current_date) as date)) and (select cast(date_trunc('week', current_date) as date) + 7)
2133                     group by stock.date
2134
2135                     union
2136
2137                select stock.date_planned, min(stock.id) as id, 0 as actual_qty, sum(stock.product_qty) as planned_qty
2138                     from stock_picking picking
2139                     inner join stock_move stock
2140                     on picking.id = stock.picking_id and picking.type = 'in'
2141                     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)
2142         group by stock.date_planned
2143                 )
2144         """)
2145 report_products_to_received_planned()
2146
2147 class report_delivery_products_planned(osv.osv):
2148     _name = "report.delivery.products.planned"
2149     _description = "Number of Delivery products vs planned"
2150     _auto = False
2151     _columns = {
2152         'date':fields.date('Date'),
2153         'qty': fields.integer('Actual Qty'),
2154         'planned_qty': fields.integer('Planned Qty'),
2155
2156     }
2157
2158     def init(self, cr):
2159         tools.drop_view_if_exists(cr, 'report_delivery_products_planned')
2160         cr.execute("""
2161             create or replace view report_delivery_products_planned as (
2162                 select stock.date, min(stock.id) as id, sum(stock.product_qty) as qty, 0 as planned_qty
2163                    from stock_picking picking
2164                     inner join stock_move stock
2165                     on picking.id = stock.picking_id and picking.type = 'out'
2166                     where stock.date between (select cast(date_trunc('week', current_date) as date)) and (select cast(date_trunc('week', current_date) as date) + 7)
2167                     group by stock.date
2168
2169                     union
2170
2171                select stock.date_planned, min(stock.id), 0 as actual_qty, sum(stock.product_qty) as planned_qty
2172                     from stock_picking picking
2173                     inner join stock_move stock
2174                     on picking.id = stock.picking_id and picking.type = 'out'
2175                     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)
2176         group by stock.date_planned
2177
2178
2179                 )
2180         """)
2181 report_delivery_products_planned()
2182 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: