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