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