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