[REM] Removed useless argument causing problem in case of buggy load_state
[odoo/odoo.git] / addons / stock / product.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 from osv import fields, osv
23 from tools.translate import _
24 import decimal_precision as dp
25
26 class product_product(osv.osv):
27     _inherit = "product.product"
28
29     def _stock_move_count(self, cr, uid, ids, field_name, arg, context=None):
30         res = dict([(id, {'reception_count': 0, 'delivery_count': 0}) for id in ids])
31         move_pool=self.pool.get('stock.move')
32         moves = move_pool.read_group(cr, uid, [
33             ('product_id', 'in', ids),
34             ('picking_id.type', '=', 'in'),
35             ('state','in',('draft','confirmed','assigned','pending'))
36         ], ['product_id'], ['product_id'])
37         for move in moves:
38             product_id = move['product_id'][0]
39             res[product_id]['reception_count'] = move['product_id_count']
40         moves = move_pool.read_group(cr, uid, [
41             ('product_id', 'in', ids),
42             ('picking_id.type', '=', 'out'),
43             ('state','in',('draft','confirmed','assigned','pending'))
44         ], ['product_id'], ['product_id'])
45         for move in moves:
46             product_id = move['product_id'][0]
47             res[product_id]['reception_count'] = move['product_id_count']
48         return res
49
50     def get_product_accounts(self, cr, uid, product_id, context=None):
51         """ To get the stock input account, stock output account and stock journal related to product.
52         @param product_id: product id
53         @return: dictionary which contains information regarding stock input account, stock output account and stock journal
54         """
55         if context is None:
56             context = {}
57         product_obj = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
58
59         stock_input_acc = product_obj.property_stock_account_input and product_obj.property_stock_account_input.id or False
60         if not stock_input_acc:
61             stock_input_acc = product_obj.categ_id.property_stock_account_input_categ and product_obj.categ_id.property_stock_account_input_categ.id or False
62
63         stock_output_acc = product_obj.property_stock_account_output and product_obj.property_stock_account_output.id or False
64         if not stock_output_acc:
65             stock_output_acc = product_obj.categ_id.property_stock_account_output_categ and product_obj.categ_id.property_stock_account_output_categ.id or False
66
67         journal_id = product_obj.categ_id.property_stock_journal and product_obj.categ_id.property_stock_journal.id or False
68         account_valuation = product_obj.categ_id.property_stock_valuation_account_id and product_obj.categ_id.property_stock_valuation_account_id.id or False
69         return {
70             'stock_account_input': stock_input_acc,
71             'stock_account_output': stock_output_acc,
72             'stock_journal': journal_id,
73             'property_stock_valuation_account_id': account_valuation
74         }
75
76     def do_change_standard_price(self, cr, uid, ids, datas, context=None):
77         """ Changes the Standard Price of Product and creates an account move accordingly.
78         @param datas : dict. contain default datas like new_price, stock_output_account, stock_input_account, stock_journal
79         @param context: A standard dictionary
80         @return:
81
82         """
83         location_obj = self.pool.get('stock.location')
84         move_obj = self.pool.get('account.move')
85         move_line_obj = self.pool.get('account.move.line')
86         if context is None:
87             context = {}
88
89         new_price = datas.get('new_price', 0.0)
90         stock_output_acc = datas.get('stock_output_account', False)
91         stock_input_acc = datas.get('stock_input_account', False)
92         journal_id = datas.get('stock_journal', False)
93         product_obj=self.browse(cr, uid, ids, context=context)[0]
94         account_valuation = product_obj.categ_id.property_stock_valuation_account_id
95         account_valuation_id = account_valuation and account_valuation.id or False
96         if not account_valuation_id: raise osv.except_osv(_('Error!'), _('Valuation Account is not specified for Product Category: %s') % (product_obj.categ_id.name))
97         move_ids = []
98         loc_ids = location_obj.search(cr, uid,[('usage','=','internal')])
99         for rec_id in ids:
100             for location in location_obj.browse(cr, uid, loc_ids, context=context):
101                 c = context.copy()
102                 c.update({
103                     'location': location.id,
104                     'compute_child': False
105                 })
106
107                 product = self.browse(cr, uid, rec_id, context=c)
108                 qty = product.qty_available
109                 diff = product.standard_price - new_price
110                 if not diff: raise osv.except_osv(_('Error!'), _("Could not find any difference between standard price and new price!"))
111                 if qty:
112                     company_id = location.company_id and location.company_id.id or False
113                     if not company_id: raise osv.except_osv(_('Error!'), _('Company is not specified in Location'))
114                     #
115                     # Accounting Entries
116                     #
117                     if not journal_id:
118                         journal_id = product.categ_id.property_stock_journal and product.categ_id.property_stock_journal.id or False
119                     if not journal_id:
120                         raise osv.except_osv(_('Error!'),
121                             _('There is no journal defined '\
122                                 'on the product category: "%s" (id: %d)') % \
123                                 (product.categ_id.name,
124                                     product.categ_id.id,))
125                     move_id = move_obj.create(cr, uid, {
126                                 'journal_id': journal_id,
127                                 'company_id': company_id
128                                 })
129
130                     move_ids.append(move_id)
131
132
133                     if diff > 0:
134                         if not stock_input_acc:
135                             stock_input_acc = product.product_tmpl_id.\
136                                 property_stock_account_input.id
137                         if not stock_input_acc:
138                             stock_input_acc = product.categ_id.\
139                                     property_stock_account_input_categ.id
140                         if not stock_input_acc:
141                             raise osv.except_osv(_('Error!'),
142                                     _('There is no stock input account defined ' \
143                                             'for this product: "%s" (id: %d)') % \
144                                             (product.name,
145                                                 product.id,))
146                         amount_diff = qty * diff
147                         move_line_obj.create(cr, uid, {
148                                     'name': product.name,
149                                     'account_id': stock_input_acc,
150                                     'debit': amount_diff,
151                                     'move_id': move_id,
152                                     })
153                         move_line_obj.create(cr, uid, {
154                                     'name': product.categ_id.name,
155                                     'account_id': account_valuation_id,
156                                     'credit': amount_diff,
157                                     'move_id': move_id
158                                     })
159                     elif diff < 0:
160                         if not stock_output_acc:
161                             stock_output_acc = product.product_tmpl_id.\
162                                 property_stock_account_output.id
163                         if not stock_output_acc:
164                             stock_output_acc = product.categ_id.\
165                                     property_stock_account_output_categ.id
166                         if not stock_output_acc:
167                             raise osv.except_osv(_('Error!'),
168                                     _('There is no stock output account defined ' \
169                                             'for this product: "%s" (id: %d)') % \
170                                             (product.name,
171                                                 product.id,))
172                         amount_diff = qty * -diff
173                         move_line_obj.create(cr, uid, {
174                                         'name': product.name,
175                                         'account_id': stock_output_acc,
176                                         'credit': amount_diff,
177                                         'move_id': move_id
178                                     })
179                         move_line_obj.create(cr, uid, {
180                                         'name': product.categ_id.name,
181                                         'account_id': account_valuation_id,
182                                         'debit': amount_diff,
183                                         'move_id': move_id
184                                     })
185
186             self.write(cr, uid, rec_id, {'standard_price': new_price})
187
188         return move_ids
189
190     def view_header_get(self, cr, user, view_id, view_type, context=None):
191         if context is None:
192             context = {}
193         res = super(product_product, self).view_header_get(cr, user, view_id, view_type, context)
194         if res: return res
195         if (context.get('active_id', False)) and (context.get('active_model') == 'stock.location'):
196             return _('Products: ')+self.pool.get('stock.location').browse(cr, user, context['active_id'], context).name
197         return res
198
199     def get_product_available(self, cr, uid, ids, context=None):
200         """ Finds whether product is available or not in particular warehouse.
201         @return: Dictionary of values
202         """
203         if context is None:
204             context = {}
205         
206         location_obj = self.pool.get('stock.location')
207         warehouse_obj = self.pool.get('stock.warehouse')
208         shop_obj = self.pool.get('sale.shop')
209         
210         states = context.get('states',[])
211         what = context.get('what',())
212         if not ids:
213             ids = self.search(cr, uid, [])
214         res = {}.fromkeys(ids, 0.0)
215         if not ids:
216             return res
217
218         if context.get('shop', False):
219             warehouse_id = shop_obj.read(cr, uid, int(context['shop']), ['warehouse_id'])['warehouse_id'][0]
220             if warehouse_id:
221                 context['warehouse'] = warehouse_id
222
223         if context.get('warehouse', False):
224             lot_id = warehouse_obj.read(cr, uid, int(context['warehouse']), ['lot_stock_id'])['lot_stock_id'][0]
225             if lot_id:
226                 context['location'] = lot_id
227
228         if context.get('location', False):
229             if type(context['location']) == type(1):
230                 location_ids = [context['location']]
231             elif type(context['location']) in (type(''), type(u'')):
232                 location_ids = location_obj.search(cr, uid, [('name','ilike',context['location'])], context=context)
233             else:
234                 location_ids = context['location']
235         else:
236             location_ids = []
237             wids = warehouse_obj.search(cr, uid, [], context=context)
238             for w in warehouse_obj.browse(cr, uid, wids, context=context):
239                 location_ids.append(w.lot_stock_id.id)
240
241         # build the list of ids of children of the location given by id
242         if context.get('compute_child',True):
243             child_location_ids = location_obj.search(cr, uid, [('location_id', 'child_of', location_ids)])
244             location_ids = child_location_ids or location_ids
245         
246         # this will be a dictionary of the UoM resources we need for conversion purposes, by UoM id
247         uoms_o = {}
248         # this will be a dictionary of the product UoM by product id
249         product2uom = {}
250         for product in self.browse(cr, uid, ids, context=context):
251             product2uom[product.id] = product.uom_id.id
252             uoms_o[product.uom_id.id] = product.uom_id
253
254         results = []
255         results2 = []
256
257         from_date = context.get('from_date',False)
258         to_date = context.get('to_date',False)
259         date_str = False
260         date_values = False
261         where = [tuple(location_ids),tuple(location_ids),tuple(ids),tuple(states)]
262         if from_date and to_date:
263             date_str = "date>=%s and date<=%s"
264             where.append(tuple([from_date]))
265             where.append(tuple([to_date]))
266         elif from_date:
267             date_str = "date>=%s"
268             date_values = [from_date]
269         elif to_date:
270             date_str = "date<=%s"
271             date_values = [to_date]
272
273         prodlot_id = context.get('prodlot_id', False)
274
275     # TODO: perhaps merge in one query.
276         if date_values:
277             where.append(tuple(date_values))
278         if 'in' in what:
279             # all moves from a location out of the set to a location in the set
280             cr.execute(
281                 'select sum(product_qty), product_id, product_uom '\
282                 'from stock_move '\
283                 'where location_id NOT IN %s '\
284                 'and location_dest_id IN %s '\
285                 'and product_id IN %s '\
286                 '' + (prodlot_id and ('and prodlot_id = ' + str(prodlot_id)) or '') + ' '\
287                 'and state IN %s ' + (date_str and 'and '+date_str+' ' or '') +' '\
288                 'group by product_id,product_uom',tuple(where))
289             results = cr.fetchall()
290         if 'out' in what:
291             # all moves from a location in the set to a location out of the set
292             cr.execute(
293                 'select sum(product_qty), product_id, product_uom '\
294                 'from stock_move '\
295                 'where location_id IN %s '\
296                 'and location_dest_id NOT IN %s '\
297                 'and product_id  IN %s '\
298                 '' + (prodlot_id and ('and prodlot_id = ' + str(prodlot_id)) or '') + ' '\
299                 'and state in %s ' + (date_str and 'and '+date_str+' ' or '') + ' '\
300                 'group by product_id,product_uom',tuple(where))
301             results2 = cr.fetchall()
302             
303         # Get the missing UoM resources
304         uom_obj = self.pool.get('product.uom')
305         uoms = map(lambda x: x[2], results) + map(lambda x: x[2], results2)
306         if context.get('uom', False):
307             uoms += [context['uom']]
308         uoms = filter(lambda x: x not in uoms_o.keys(), uoms)
309         if uoms:
310             uoms = uom_obj.browse(cr, uid, list(set(uoms)), context=context)
311             for o in uoms:
312                 uoms_o[o.id] = o
313                 
314         #TOCHECK: before change uom of product, stock move line are in old uom.
315         context.update({'raise-exception': False})
316         # Count the incoming quantities
317         for amount, prod_id, prod_uom in results:
318             amount = uom_obj._compute_qty_obj(cr, uid, uoms_o[prod_uom], amount,
319                      uoms_o[context.get('uom', False) or product2uom[prod_id]], context=context)
320             res[prod_id] += amount
321         # Count the outgoing quantities
322         for amount, prod_id, prod_uom in results2:
323             amount = uom_obj._compute_qty_obj(cr, uid, uoms_o[prod_uom], amount,
324                     uoms_o[context.get('uom', False) or product2uom[prod_id]], context=context)
325             res[prod_id] -= amount
326         return res
327
328     def _product_available(self, cr, uid, ids, field_names=None, arg=False, context=None):
329         """ Finds the incoming and outgoing quantity of product.
330         @return: Dictionary of values
331         """
332         if not field_names:
333             field_names = []
334         if context is None:
335             context = {}
336         res = {}
337         for id in ids:
338             res[id] = {}.fromkeys(field_names, 0.0)
339         for f in field_names:
340             c = context.copy()
341             if f == 'qty_available':
342                 c.update({ 'states': ('done',), 'what': ('in', 'out') })
343             if f == 'virtual_available':
344                 c.update({ 'states': ('confirmed','waiting','assigned','done'), 'what': ('in', 'out') })
345             if f == 'incoming_qty':
346                 c.update({ 'states': ('confirmed','waiting','assigned'), 'what': ('in',) })
347             if f == 'outgoing_qty':
348                 c.update({ 'states': ('confirmed','waiting','assigned'), 'what': ('out',) })
349             stock = self.get_product_available(cr, uid, ids, context=c)
350             for id in ids:
351                 res[id][f] = stock.get(id, 0.0)
352         return res
353
354     _columns = {
355         'reception_count': fields.function(_stock_move_count, string="Reception", type='integer', multi='pickings'),
356         'delivery_count': fields.function(_stock_move_count, string="Delivery", type='integer', multi='pickings'),
357         'qty_available': fields.function(_product_available, multi='qty_available',
358             type='float',  digits_compute=dp.get_precision('Product Unit of Measure'),
359             string='Quantity On Hand',
360             help="Current quantity of products.\n"
361                  "In a context with a single Stock Location, this includes "
362                  "goods stored at this Location, or any of its children.\n"
363                  "In a context with a single Warehouse, this includes "
364                  "goods stored in the Stock Location of this Warehouse, or any "
365                  "of its children.\n"
366                  "In a context with a single Shop, this includes goods "
367                  "stored in the Stock Location of the Warehouse of this Shop, "
368                  "or any of its children.\n"
369                  "Otherwise, this includes goods stored in any Stock Location "
370                  "typed as 'internal'."),
371         'virtual_available': fields.function(_product_available, multi='qty_available',
372             type='float',  digits_compute=dp.get_precision('Product Unit of Measure'),
373             string='Quantity Available',
374             help="Forecast quantity (computed as Quantity On Hand "
375                  "- Outgoing + Incoming)\n"
376                  "In a context with a single Stock Location, this includes "
377                  "goods stored at this Location, or any of its children.\n"
378                  "In a context with a single Warehouse, this includes "
379                  "goods stored in the Stock Location of this Warehouse, or any "
380                  "of its children.\n"
381                  "In a context with a single Shop, this includes goods "
382                  "stored in the Stock Location of the Warehouse of this Shop, "
383                  "or any of its children.\n"
384                  "Otherwise, this includes goods stored in any Stock Location "
385                  "typed as 'internal'."),
386         'incoming_qty': fields.function(_product_available, multi='qty_available',
387             type='float',  digits_compute=dp.get_precision('Product Unit of Measure'),
388             string='Incoming',
389             help="Quantity of products that are planned to arrive.\n"
390                  "In a context with a single Stock Location, this includes "
391                  "goods arriving to this Location, or any of its children.\n"
392                  "In a context with a single Warehouse, this includes "
393                  "goods arriving to the Stock Location of this Warehouse, or "
394                  "any of its children.\n"
395                  "In a context with a single Shop, this includes goods "
396                  "arriving to the Stock Location of the Warehouse of this "
397                  "Shop, or any of its children.\n"
398                  "Otherwise, this includes goods arriving to any Stock "
399                  "Location typed as 'internal'."),
400         'outgoing_qty': fields.function(_product_available, multi='qty_available',
401             type='float',  digits_compute=dp.get_precision('Product Unit of Measure'),
402             string='Outgoing',
403             help="Quantity of products that are planned to leave.\n"
404                  "In a context with a single Stock Location, this includes "
405                  "goods leaving from this Location, or any of its children.\n"
406                  "In a context with a single Warehouse, this includes "
407                  "goods leaving from the Stock Location of this Warehouse, or "
408                  "any of its children.\n"
409                  "In a context with a single Shop, this includes goods "
410                  "leaving from the Stock Location of the Warehouse of this "
411                  "Shop, or any of its children.\n"
412                  "Otherwise, this includes goods leaving from any Stock "
413                  "Location typed as 'internal'."),
414         'track_production': fields.boolean('Track Manufacturing Lots', help="Forces to specify a Serial Number for all moves containing this product and generated by a Manufacturing Order"),
415         'track_incoming': fields.boolean('Track Incoming Lots', help="Forces to specify a Serial Number for all moves containing this product and coming from a Supplier Location"),
416         'track_outgoing': fields.boolean('Track Outgoing Lots', help="Forces to specify a Serial Number for all moves containing this product and going to a Customer Location"),
417         'location_id': fields.dummy(string='Location', relation='stock.location', type='many2one'),
418         'warehouse_id': fields.dummy(string='Warehouse', relation='stock.warehouse', type='many2one'),
419         'valuation':fields.selection([('manual_periodic', 'Periodical (manual)'),
420                                         ('real_time','Real Time (automated)'),], 'Inventory Valuation',
421                                         help="If real-time valuation is enabled for a product, the system will automatically write journal entries corresponding to stock moves." \
422                                              "The inventory variation account set on the product category will represent the current inventory value, and the stock input and stock output account will hold the counterpart moves for incoming and outgoing products."
423                                         , required=True),
424     }
425
426     _defaults = {
427         'valuation': 'manual_periodic',
428     }
429
430     def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
431         res = super(product_product,self).fields_view_get(cr, uid, view_id, view_type, context, toolbar=toolbar, submenu=submenu)
432         if context is None:
433             context = {}
434         if ('location' in context) and context['location']:
435             location_info = self.pool.get('stock.location').browse(cr, uid, context['location'])
436             fields=res.get('fields',{})
437             if fields:
438                 if location_info.usage == 'supplier':
439                     if fields.get('virtual_available'):
440                         res['fields']['virtual_available']['string'] = _('Future Receptions')
441                     if fields.get('qty_available'):
442                         res['fields']['qty_available']['string'] = _('Received Qty')
443
444                 if location_info.usage == 'internal':
445                     if fields.get('virtual_available'):
446                         res['fields']['virtual_available']['string'] = _('Future Stock')
447
448                 if location_info.usage == 'customer':
449                     if fields.get('virtual_available'):
450                         res['fields']['virtual_available']['string'] = _('Future Deliveries')
451                     if fields.get('qty_available'):
452                         res['fields']['qty_available']['string'] = _('Delivered Qty')
453
454                 if location_info.usage == 'inventory':
455                     if fields.get('virtual_available'):
456                         res['fields']['virtual_available']['string'] = _('Future P&L')
457                     if fields.get('qty_available'):
458                         res['fields']['qty_available']['string'] = _('P&L Qty')
459
460                 if location_info.usage == 'procurement':
461                     if fields.get('virtual_available'):
462                         res['fields']['virtual_available']['string'] = _('Future Qty')
463                     if fields.get('qty_available'):
464                         res['fields']['qty_available']['string'] = _('Unplanned Qty')
465
466                 if location_info.usage == 'production':
467                     if fields.get('virtual_available'):
468                         res['fields']['virtual_available']['string'] = _('Future Productions')
469                     if fields.get('qty_available'):
470                         res['fields']['qty_available']['string'] = _('Produced Qty')
471         return res
472
473 product_product()
474
475 class product_template(osv.osv):
476     _name = 'product.template'
477     _inherit = 'product.template'
478     _columns = {
479         'property_stock_procurement': fields.property(
480             'stock.location',
481             type='many2one',
482             relation='stock.location',
483             string="Procurement Location",
484             view_load=True,
485             domain=[('usage','like','procurement')],
486             help="For the current product, this stock location will be used, instead of the default one, as the source location for stock moves generated by procurements"),
487         'property_stock_production': fields.property(
488             'stock.location',
489             type='many2one',
490             relation='stock.location',
491             string="Production Location",
492             view_load=True,
493             domain=[('usage','like','production')],
494             help="For the current product, this stock location will be used, instead of the default one, as the source location for stock moves generated by production orders"),
495         'property_stock_inventory': fields.property(
496             'stock.location',
497             type='many2one',
498             relation='stock.location',
499             string="Inventory Location",
500             view_load=True,
501             domain=[('usage','like','inventory')],
502             help="For the current product, this stock location will be used, instead of the default one, as the source location for stock moves generated when you do an inventory"),
503         'property_stock_account_input': fields.property('account.account',
504             type='many2one', relation='account.account',
505             string='Stock Input Account', view_load=True,
506             help="When doing real-time inventory valuation, counterpart journal items for all incoming stock moves will be posted in this account, unless "
507                  "there is a specific valuation account set on the source location. When not set on the product, the one from the product category is used."),
508         'property_stock_account_output': fields.property('account.account',
509             type='many2one', relation='account.account',
510             string='Stock Output Account', view_load=True,
511             help="When doing real-time inventory valuation, counterpart journal items for all outgoing stock moves will be posted in this account, unless "
512                  "there is a specific valuation account set on the destination location. When not set on the product, the one from the product category is used."),
513     }
514
515 product_template()
516
517 class product_category(osv.osv):
518
519     _inherit = 'product.category'
520     _columns = {
521         'property_stock_journal': fields.property('account.journal',
522             relation='account.journal', type='many2one',
523             string='Stock journal', view_load=True,
524             help="When doing real-time inventory valuation, this is the Accounting Journal in which entries will be automatically posted when stock moves are processed."),
525         'property_stock_account_input_categ': fields.property('account.account',
526             type='many2one', relation='account.account',
527             string='Stock Input Account', view_load=True,
528             help="When doing real-time inventory valuation, counterpart journal items for all incoming stock moves will be posted in this account, unless "
529                  "there is a specific valuation account set on the source location. This is the default value for all products in this category. It "
530                  "can also directly be set on each product"),
531         'property_stock_account_output_categ': fields.property('account.account',
532             type='many2one', relation='account.account',
533             string='Stock Output Account', view_load=True,
534             help="When doing real-time inventory valuation, counterpart journal items for all outgoing stock moves will be posted in this account, unless "
535                  "there is a specific valuation account set on the destination location. This is the default value for all products in this category. It "
536                  "can also directly be set on each product"),
537         'property_stock_valuation_account_id': fields.property('account.account',
538             type='many2one',
539             relation='account.account',
540             string="Stock Valuation Account",
541             view_load=True,
542             help="When real-time inventory valuation is enabled on a product, this account will hold the current value of the products.",),
543     }
544
545 product_category()
546
547 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: