[FIX] stock: inventory lines with no production lot: compare with correct stock level
[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 openerp.osv import fields, osv
23 from openerp.tools.translate import _
24 import openerp.addons.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',('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',('confirmed','assigned','pending'))
44         ], ['product_id'], ['product_id'])
45         for move in moves:
46             product_id = move['product_id'][0]
47             res[product_id]['delivery_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         move_ids = []
94         loc_ids = location_obj.search(cr, uid,[('usage','=','internal')])
95         for product in self.browse(cr, uid, ids, context=context):
96             if product.valuation != 'real_time':
97                 continue
98             account_valuation = product.categ_id.property_stock_valuation_account_id
99             account_valuation_id = account_valuation and account_valuation.id or False
100             if not account_valuation_id: raise osv.except_osv(_('Error!'), _('Specify valuation Account for Product Category: %s.') % (product.categ_id.name))
101             for location in location_obj.browse(cr, uid, loc_ids, context=context):
102                 c = context.copy()
103                 c.update({
104                     'location': location.id,
105                     'compute_child': False
106                 })
107
108                 # qty_available depends of the location in the context
109                 qty = self.read(cr, uid, [product.id], ['qty_available'], context=c)[0]['qty_available']
110
111                 diff = product.standard_price - new_price
112                 if not diff: raise osv.except_osv(_('Error!'), _("No difference between standard price and new price!"))
113                 if qty:
114                     company_id = location.company_id and location.company_id.id or False
115                     if not company_id: raise osv.except_osv(_('Error!'), _('Please specify company in Location.'))
116                     #
117                     # Accounting Entries
118                     #
119                     if not journal_id:
120                         journal_id = product.categ_id.property_stock_journal and product.categ_id.property_stock_journal.id or False
121                     if not journal_id:
122                         raise osv.except_osv(_('Error!'),
123                             _('Please define journal '\
124                               'on the product category: "%s" (id: %d).') % \
125                                 (product.categ_id.name,
126                                     product.categ_id.id,))
127                     move_id = move_obj.create(cr, uid, {
128                                 'journal_id': journal_id,
129                                 'company_id': company_id
130                                 })
131
132                     move_ids.append(move_id)
133
134
135                     if diff > 0:
136                         if not stock_input_acc:
137                             stock_input_acc = product.\
138                                 property_stock_account_input.id
139                         if not stock_input_acc:
140                             stock_input_acc = product.categ_id.\
141                                     property_stock_account_input_categ.id
142                         if not stock_input_acc:
143                             raise osv.except_osv(_('Error!'),
144                                     _('Please define stock input account ' \
145                                             'for this product: "%s" (id: %d).') % \
146                                             (product.name,
147                                                 product.id,))
148                         amount_diff = qty * diff
149                         move_line_obj.create(cr, uid, {
150                                     'name': product.name,
151                                     'account_id': stock_input_acc,
152                                     'debit': amount_diff,
153                                     'move_id': move_id,
154                                     })
155                         move_line_obj.create(cr, uid, {
156                                     'name': product.categ_id.name,
157                                     'account_id': account_valuation_id,
158                                     'credit': amount_diff,
159                                     'move_id': move_id
160                                     })
161                     elif diff < 0:
162                         if not stock_output_acc:
163                             stock_output_acc = product.\
164                                 property_stock_account_output.id
165                         if not stock_output_acc:
166                             stock_output_acc = product.categ_id.\
167                                     property_stock_account_output_categ.id
168                         if not stock_output_acc:
169                             raise osv.except_osv(_('Error!'),
170                                     _('Please define stock output account ' \
171                                             'for this product: "%s" (id: %d).') % \
172                                             (product.name,
173                                                 product.id,))
174                         amount_diff = qty * -diff
175                         move_line_obj.create(cr, uid, {
176                                         'name': product.name,
177                                         'account_id': stock_output_acc,
178                                         'credit': amount_diff,
179                                         'move_id': move_id
180                                     })
181                         move_line_obj.create(cr, uid, {
182                                         'name': product.categ_id.name,
183                                         'account_id': account_valuation_id,
184                                         'debit': amount_diff,
185                                         'move_id': move_id
186                                     })
187         self.write(cr, uid, ids, {'standard_price': new_price})
188
189         return move_ids
190
191     def view_header_get(self, cr, user, view_id, view_type, context=None):
192         if context is None:
193             context = {}
194         res = super(product_product, self).view_header_get(cr, user, view_id, view_type, context)
195         if res: return res
196         if (context.get('active_id', False)) and (context.get('active_model') == 'stock.location'):
197             return _('Products: ')+self.pool.get('stock.location').browse(cr, user, context['active_id'], context).name
198         return res
199
200     def get_product_available(self, cr, uid, ids, context=None):
201         """ Finds whether product is available or not in particular warehouse.
202         @return: Dictionary of values
203         """
204         if context is None:
205             context = {}
206         
207         location_obj = self.pool.get('stock.location')
208         warehouse_obj = self.pool.get('stock.warehouse')
209         shop_obj = self.pool.get('sale.shop')
210         
211         states = context.get('states',[])
212         what = context.get('what',())
213         if not ids:
214             ids = self.search(cr, uid, [])
215         res = {}.fromkeys(ids, 0.0)
216         if not ids:
217             return res
218
219         if context.get('shop', False):
220             warehouse_id = shop_obj.read(cr, uid, int(context['shop']), ['warehouse_id'])['warehouse_id'][0]
221             if warehouse_id:
222                 context['warehouse'] = warehouse_id
223
224         if context.get('warehouse', False):
225             lot_id = warehouse_obj.read(cr, uid, int(context['warehouse']), ['lot_stock_id'])['lot_stock_id'][0]
226             if lot_id:
227                 context['location'] = lot_id
228
229         if context.get('location', False):
230             if type(context['location']) == type(1):
231                 location_ids = [context['location']]
232             elif type(context['location']) in (type(''), type(u'')):
233                 location_ids = location_obj.search(cr, uid, [('name','ilike',context['location'])], context=context)
234             else:
235                 location_ids = context['location']
236         else:
237             location_ids = []
238             wids = warehouse_obj.search(cr, uid, [], context=context)
239             if not wids:
240                 return res
241             for w in warehouse_obj.browse(cr, uid, wids, context=context):
242                 location_ids.append(w.lot_stock_id.id)
243
244         # build the list of ids of children of the location given by id
245         if context.get('compute_child',True):
246             child_location_ids = location_obj.search(cr, uid, [('location_id', 'child_of', location_ids)])
247             location_ids = child_location_ids or location_ids
248         
249         # this will be a dictionary of the product UoM by product id
250         product2uom = {}
251         uom_ids = []
252         for product in self.read(cr, uid, ids, ['uom_id'], context=context):
253             product2uom[product['id']] = product['uom_id'][0]
254             uom_ids.append(product['uom_id'][0])
255         # this will be a dictionary of the UoM resources we need for conversion purposes, by UoM id
256         uoms_o = {}
257         for uom in self.pool.get('product.uom').browse(cr, uid, uom_ids, context=context):
258             uoms_o[uom.id] = uom
259
260         results = []
261         results2 = []
262
263         from_date = context.get('from_date',False)
264         to_date = context.get('to_date',False)
265         date_str = False
266         date_values = False
267         where = [tuple(location_ids),tuple(location_ids),tuple(ids),tuple(states)]
268         if from_date and to_date:
269             date_str = "date>=%s and date<=%s"
270             where.append(tuple([from_date]))
271             where.append(tuple([to_date]))
272         elif from_date:
273             date_str = "date>=%s"
274             date_values = [from_date]
275         elif to_date:
276             date_str = "date<=%s"
277             date_values = [to_date]
278         if date_values:
279             where.append(tuple(date_values))
280
281         prodlot_id = context.get('prodlot_id', False)
282         prodlot_clause = ''
283         if prodlot_id:
284             prodlot_clause = ' and prodlot_id = %s '
285             where += [prodlot_id]
286         elif 'prodlot_id' in context and not prodlot_id:
287             prodlot_clause = ' and prodlot_id is null '
288
289         # TODO: perhaps merge in one query.
290         if 'in' in what:
291             # all moves from a location out of the set to a location in the set
292             cr.execute(
293                 'select sum(product_qty), product_id, product_uom '\
294                 'from stock_move '\
295                 'where location_id NOT IN %s '\
296                 'and location_dest_id IN %s '\
297                 'and product_id IN %s '\
298                 'and state IN %s ' + (date_str and 'and '+date_str+' ' or '') +' '\
299                 + prodlot_clause + 
300                 'group by product_id,product_uom',tuple(where))
301             results = cr.fetchall()
302         if 'out' in what:
303             # all moves from a location in the set to a location out of the set
304             cr.execute(
305                 'select sum(product_qty), product_id, product_uom '\
306                 'from stock_move '\
307                 'where location_id IN %s '\
308                 'and location_dest_id NOT IN %s '\
309                 'and product_id  IN %s '\
310                 'and state in %s ' + (date_str and 'and '+date_str+' ' or '') + ' '\
311                 + prodlot_clause + 
312                 'group by product_id,product_uom',tuple(where))
313             results2 = cr.fetchall()
314             
315         # Get the missing UoM resources
316         uom_obj = self.pool.get('product.uom')
317         uoms = map(lambda x: x[2], results) + map(lambda x: x[2], results2)
318         if context.get('uom', False):
319             uoms += [context['uom']]
320         uoms = filter(lambda x: x not in uoms_o.keys(), uoms)
321         if uoms:
322             uoms = uom_obj.browse(cr, uid, list(set(uoms)), context=context)
323             for o in uoms:
324                 uoms_o[o.id] = o
325                 
326         #TOCHECK: before change uom of product, stock move line are in old uom.
327         context.update({'raise-exception': False})
328         # Count the incoming quantities
329         for amount, prod_id, prod_uom in results:
330             amount = uom_obj._compute_qty_obj(cr, uid, uoms_o[prod_uom], amount,
331                      uoms_o[context.get('uom', False) or product2uom[prod_id]], context=context)
332             res[prod_id] += amount
333         # Count the outgoing quantities
334         for amount, prod_id, prod_uom in results2:
335             amount = uom_obj._compute_qty_obj(cr, uid, uoms_o[prod_uom], amount,
336                     uoms_o[context.get('uom', False) or product2uom[prod_id]], context=context)
337             res[prod_id] -= amount
338         return res
339
340     def _product_available(self, cr, uid, ids, field_names=None, arg=False, context=None):
341         """ Finds the incoming and outgoing quantity of product.
342         @return: Dictionary of values
343         """
344         if not field_names:
345             field_names = []
346         if context is None:
347             context = {}
348         res = {}
349         for id in ids:
350             res[id] = {}.fromkeys(field_names, 0.0)
351         for f in field_names:
352             c = context.copy()
353             if f == 'qty_available':
354                 c.update({ 'states': ('done',), 'what': ('in', 'out') })
355             if f == 'virtual_available':
356                 c.update({ 'states': ('confirmed','waiting','assigned','done'), 'what': ('in', 'out') })
357             if f == 'incoming_qty':
358                 c.update({ 'states': ('confirmed','waiting','assigned'), 'what': ('in',) })
359             if f == 'outgoing_qty':
360                 c.update({ 'states': ('confirmed','waiting','assigned'), 'what': ('out',) })
361             stock = self.get_product_available(cr, uid, ids, context=c)
362             for id in ids:
363                 res[id][f] = stock.get(id, 0.0)
364         return res
365
366     _columns = {
367         'reception_count': fields.function(_stock_move_count, string="Reception", type='integer', multi='pickings'),
368         'delivery_count': fields.function(_stock_move_count, string="Delivery", type='integer', multi='pickings'),
369         'qty_available': fields.function(_product_available, multi='qty_available',
370             type='float',  digits_compute=dp.get_precision('Product Unit of Measure'),
371             string='Quantity On Hand',
372             help="Current quantity of products.\n"
373                  "In a context with a single Stock Location, this includes "
374                  "goods stored at this Location, or any of its children.\n"
375                  "In a context with a single Warehouse, this includes "
376                  "goods stored in the Stock Location of this Warehouse, or any "
377                  "of its children.\n"
378                  "In a context with a single Shop, this includes goods "
379                  "stored in the Stock Location of the Warehouse of this Shop, "
380                  "or any of its children.\n"
381                  "Otherwise, this includes goods stored in any Stock Location "
382                  "with 'internal' type."),
383         'virtual_available': fields.function(_product_available, multi='qty_available',
384             type='float',  digits_compute=dp.get_precision('Product Unit of Measure'),
385             string='Forecasted Quantity',
386             help="Forecast quantity (computed as Quantity On Hand "
387                  "- Outgoing + Incoming)\n"
388                  "In a context with a single Stock Location, this includes "
389                  "goods stored in this location, or any of its children.\n"
390                  "In a context with a single Warehouse, this includes "
391                  "goods stored in the Stock Location of this Warehouse, or any "
392                  "of its children.\n"
393                  "In a context with a single Shop, this includes goods "
394                  "stored in the Stock Location of the Warehouse of this Shop, "
395                  "or any of its children.\n"
396                  "Otherwise, this includes goods stored in any Stock Location "
397                  "with 'internal' type."),
398         'incoming_qty': fields.function(_product_available, multi='qty_available',
399             type='float',  digits_compute=dp.get_precision('Product Unit of Measure'),
400             string='Incoming',
401             help="Quantity of products that are planned to arrive.\n"
402                  "In a context with a single Stock Location, this includes "
403                  "goods arriving to this Location, or any of its children.\n"
404                  "In a context with a single Warehouse, this includes "
405                  "goods arriving to the Stock Location of this Warehouse, or "
406                  "any of its children.\n"
407                  "In a context with a single Shop, this includes goods "
408                  "arriving to the Stock Location of the Warehouse of this "
409                  "Shop, or any of its children.\n"
410                  "Otherwise, this includes goods arriving to any Stock "
411                  "Location with 'internal' type."),
412         'outgoing_qty': fields.function(_product_available, multi='qty_available',
413             type='float',  digits_compute=dp.get_precision('Product Unit of Measure'),
414             string='Outgoing',
415             help="Quantity of products that are planned to leave.\n"
416                  "In a context with a single Stock Location, this includes "
417                  "goods leaving this Location, or any of its children.\n"
418                  "In a context with a single Warehouse, this includes "
419                  "goods leaving the Stock Location of this Warehouse, or "
420                  "any of its children.\n"
421                  "In a context with a single Shop, this includes goods "
422                  "leaving the Stock Location of the Warehouse of this "
423                  "Shop, or any of its children.\n"
424                  "Otherwise, this includes goods leaving any Stock "
425                  "Location with 'internal' type."),
426         '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"),
427         '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"),
428         '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"),
429         'location_id': fields.dummy(string='Location', relation='stock.location', type='many2one'),
430         'warehouse_id': fields.dummy(string='Warehouse', relation='stock.warehouse', type='many2one'),
431         'valuation':fields.selection([('manual_periodic', 'Periodical (manual)'),
432                                         ('real_time','Real Time (automated)'),], 'Inventory Valuation',
433                                         help="If real-time valuation is enabled for a product, the system will automatically write journal entries corresponding to stock moves." \
434                                              "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."
435                                         , required=True),
436     }
437
438     _defaults = {
439         'valuation': 'manual_periodic',
440     }
441
442     def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
443         res = super(product_product,self).fields_view_get(cr, uid, view_id, view_type, context, toolbar=toolbar, submenu=submenu)
444         if context is None:
445             context = {}
446         if ('location' in context) and context['location']:
447             location_info = self.pool.get('stock.location').browse(cr, uid, context['location'])
448             fields=res.get('fields',{})
449             if fields:
450                 if location_info.usage == 'supplier':
451                     if fields.get('virtual_available'):
452                         res['fields']['virtual_available']['string'] = _('Future Receptions')
453                     if fields.get('qty_available'):
454                         res['fields']['qty_available']['string'] = _('Received Qty')
455
456                 if location_info.usage == 'internal':
457                     if fields.get('virtual_available'):
458                         res['fields']['virtual_available']['string'] = _('Future Stock')
459
460                 if location_info.usage == 'customer':
461                     if fields.get('virtual_available'):
462                         res['fields']['virtual_available']['string'] = _('Future Deliveries')
463                     if fields.get('qty_available'):
464                         res['fields']['qty_available']['string'] = _('Delivered Qty')
465
466                 if location_info.usage == 'inventory':
467                     if fields.get('virtual_available'):
468                         res['fields']['virtual_available']['string'] = _('Future P&L')
469                     if fields.get('qty_available'):
470                         res['fields']['qty_available']['string'] = _('P&L Qty')
471
472                 if location_info.usage == 'procurement':
473                     if fields.get('virtual_available'):
474                         res['fields']['virtual_available']['string'] = _('Future Qty')
475                     if fields.get('qty_available'):
476                         res['fields']['qty_available']['string'] = _('Unplanned Qty')
477
478                 if location_info.usage == 'production':
479                     if fields.get('virtual_available'):
480                         res['fields']['virtual_available']['string'] = _('Future Productions')
481                     if fields.get('qty_available'):
482                         res['fields']['qty_available']['string'] = _('Produced Qty')
483         return res
484
485 product_product()
486
487 class product_template(osv.osv):
488     _name = 'product.template'
489     _inherit = 'product.template'
490     _columns = {
491         'property_stock_procurement': fields.property(
492             'stock.location',
493             type='many2one',
494             relation='stock.location',
495             string="Procurement Location",
496             view_load=True,
497             domain=[('usage','like','procurement')],
498             help="This stock location will be used, instead of the default one, as the source location for stock moves generated by procurements."),
499         'property_stock_production': fields.property(
500             'stock.location',
501             type='many2one',
502             relation='stock.location',
503             string="Production Location",
504             view_load=True,
505             domain=[('usage','like','production')],
506             help="This stock location will be used, instead of the default one, as the source location for stock moves generated by manufacturing orders."),
507         'property_stock_inventory': fields.property(
508             'stock.location',
509             type='many2one',
510             relation='stock.location',
511             string="Inventory Location",
512             view_load=True,
513             domain=[('usage','like','inventory')],
514             help="This stock location will be used, instead of the default one, as the source location for stock moves generated when you do an inventory."),
515         'property_stock_account_input': fields.property('account.account',
516             type='many2one', relation='account.account',
517             string='Stock Input Account', view_load=True,
518             help="When doing real-time inventory valuation, counterpart journal items for all incoming stock moves will be posted in this account, unless "
519                  "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."),
520         'property_stock_account_output': fields.property('account.account',
521             type='many2one', relation='account.account',
522             string='Stock Output Account', view_load=True,
523             help="When doing real-time inventory valuation, counterpart journal items for all outgoing stock moves will be posted in this account, unless "
524                  "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."),
525         'sale_delay': fields.float('Customer Lead Time', help="The average delay in days between the confirmation of the customer order and the delivery of the finished products. It's the time you promise to your customers."),
526         'loc_rack': fields.char('Rack', size=16),
527         'loc_row': fields.char('Row', size=16),
528         'loc_case': fields.char('Case', size=16),
529     }
530
531     _defaults = {
532         'sale_delay': 7,
533     }
534 product_template()
535
536 class product_category(osv.osv):
537
538     _inherit = 'product.category'
539     _columns = {
540         'property_stock_journal': fields.property('account.journal',
541             relation='account.journal', type='many2one',
542             string='Stock Journal', view_load=True,
543             help="When doing real-time inventory valuation, this is the Accounting Journal in which entries will be automatically posted when stock moves are processed."),
544         'property_stock_account_input_categ': fields.property('account.account',
545             type='many2one', relation='account.account',
546             string='Stock Input Account', view_load=True,
547             help="When doing real-time inventory valuation, counterpart journal items for all incoming stock moves will be posted in this account, unless "
548                  "there is a specific valuation account set on the source location. This is the default value for all products in this category. It "
549                  "can also directly be set on each product"),
550         'property_stock_account_output_categ': fields.property('account.account',
551             type='many2one', relation='account.account',
552             string='Stock Output Account', view_load=True,
553             help="When doing real-time inventory valuation, counterpart journal items for all outgoing stock moves will be posted in this account, unless "
554                  "there is a specific valuation account set on the destination location. This is the default value for all products in this category. It "
555                  "can also directly be set on each product"),
556         'property_stock_valuation_account_id': fields.property('account.account',
557             type='many2one',
558             relation='account.account',
559             string="Stock Valuation Account",
560             view_load=True,
561             help="When real-time inventory valuation is enabled on a product, this account will hold the current value of the products.",),
562     }
563
564 product_category()
565
566 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: