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