[FIX] stock: recompute the qty_available based on the location instead of using the...
[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
287         # TODO: perhaps merge in one query.
288         if 'in' in what:
289             # all moves from a location out of the set to a location in the set
290             cr.execute(
291                 'select sum(product_qty), product_id, product_uom '\
292                 'from stock_move '\
293                 'where location_id NOT IN %s '\
294                 'and location_dest_id IN %s '\
295                 'and product_id IN %s '\
296                 'and state IN %s ' + (date_str and 'and '+date_str+' ' or '') +' '\
297                 + prodlot_clause + 
298                 'group by product_id,product_uom',tuple(where))
299             results = cr.fetchall()
300         if 'out' in what:
301             # all moves from a location in the set to a location out of the set
302             cr.execute(
303                 'select sum(product_qty), product_id, product_uom '\
304                 'from stock_move '\
305                 'where location_id IN %s '\
306                 'and location_dest_id NOT IN %s '\
307                 'and product_id  IN %s '\
308                 'and state in %s ' + (date_str and 'and '+date_str+' ' or '') + ' '\
309                 + prodlot_clause + 
310                 'group by product_id,product_uom',tuple(where))
311             results2 = cr.fetchall()
312             
313         # Get the missing UoM resources
314         uom_obj = self.pool.get('product.uom')
315         uoms = map(lambda x: x[2], results) + map(lambda x: x[2], results2)
316         if context.get('uom', False):
317             uoms += [context['uom']]
318         uoms = filter(lambda x: x not in uoms_o.keys(), uoms)
319         if uoms:
320             uoms = uom_obj.browse(cr, uid, list(set(uoms)), context=context)
321             for o in uoms:
322                 uoms_o[o.id] = o
323                 
324         #TOCHECK: before change uom of product, stock move line are in old uom.
325         context.update({'raise-exception': False})
326         # Count the incoming quantities
327         for amount, prod_id, prod_uom in results:
328             amount = uom_obj._compute_qty_obj(cr, uid, uoms_o[prod_uom], amount,
329                      uoms_o[context.get('uom', False) or product2uom[prod_id]], context=context)
330             res[prod_id] += amount
331         # Count the outgoing quantities
332         for amount, prod_id, prod_uom in results2:
333             amount = uom_obj._compute_qty_obj(cr, uid, uoms_o[prod_uom], amount,
334                     uoms_o[context.get('uom', False) or product2uom[prod_id]], context=context)
335             res[prod_id] -= amount
336         return res
337
338     def _product_available(self, cr, uid, ids, field_names=None, arg=False, context=None):
339         """ Finds the incoming and outgoing quantity of product.
340         @return: Dictionary of values
341         """
342         if not field_names:
343             field_names = []
344         if context is None:
345             context = {}
346         res = {}
347         for id in ids:
348             res[id] = {}.fromkeys(field_names, 0.0)
349         for f in field_names:
350             c = context.copy()
351             if f == 'qty_available':
352                 c.update({ 'states': ('done',), 'what': ('in', 'out') })
353             if f == 'virtual_available':
354                 c.update({ 'states': ('confirmed','waiting','assigned','done'), 'what': ('in', 'out') })
355             if f == 'incoming_qty':
356                 c.update({ 'states': ('confirmed','waiting','assigned'), 'what': ('in',) })
357             if f == 'outgoing_qty':
358                 c.update({ 'states': ('confirmed','waiting','assigned'), 'what': ('out',) })
359             stock = self.get_product_available(cr, uid, ids, context=c)
360             for id in ids:
361                 res[id][f] = stock.get(id, 0.0)
362         return res
363
364     _columns = {
365         'reception_count': fields.function(_stock_move_count, string="Reception", type='integer', multi='pickings'),
366         'delivery_count': fields.function(_stock_move_count, string="Delivery", type='integer', multi='pickings'),
367         'qty_available': fields.function(_product_available, multi='qty_available',
368             type='float',  digits_compute=dp.get_precision('Product Unit of Measure'),
369             string='Quantity On Hand',
370             help="Current quantity of products.\n"
371                  "In a context with a single Stock Location, this includes "
372                  "goods stored at this Location, or any of its children.\n"
373                  "In a context with a single Warehouse, this includes "
374                  "goods stored in the Stock Location of this Warehouse, or any "
375                  "of its children.\n"
376                  "In a context with a single Shop, this includes goods "
377                  "stored in the Stock Location of the Warehouse of this Shop, "
378                  "or any of its children.\n"
379                  "Otherwise, this includes goods stored in any Stock Location "
380                  "with 'internal' type."),
381         'virtual_available': fields.function(_product_available, multi='qty_available',
382             type='float',  digits_compute=dp.get_precision('Product Unit of Measure'),
383             string='Forecasted Quantity',
384             help="Forecast quantity (computed as Quantity On Hand "
385                  "- Outgoing + Incoming)\n"
386                  "In a context with a single Stock Location, this includes "
387                  "goods stored in this location, or any of its children.\n"
388                  "In a context with a single Warehouse, this includes "
389                  "goods stored in the Stock Location of this Warehouse, or any "
390                  "of its children.\n"
391                  "In a context with a single Shop, this includes goods "
392                  "stored in the Stock Location of the Warehouse of this Shop, "
393                  "or any of its children.\n"
394                  "Otherwise, this includes goods stored in any Stock Location "
395                  "with 'internal' type."),
396         'incoming_qty': fields.function(_product_available, multi='qty_available',
397             type='float',  digits_compute=dp.get_precision('Product Unit of Measure'),
398             string='Incoming',
399             help="Quantity of products that are planned to arrive.\n"
400                  "In a context with a single Stock Location, this includes "
401                  "goods arriving to this Location, or any of its children.\n"
402                  "In a context with a single Warehouse, this includes "
403                  "goods arriving to the Stock Location of this Warehouse, or "
404                  "any of its children.\n"
405                  "In a context with a single Shop, this includes goods "
406                  "arriving to the Stock Location of the Warehouse of this "
407                  "Shop, or any of its children.\n"
408                  "Otherwise, this includes goods arriving to any Stock "
409                  "Location with 'internal' type."),
410         'outgoing_qty': fields.function(_product_available, multi='qty_available',
411             type='float',  digits_compute=dp.get_precision('Product Unit of Measure'),
412             string='Outgoing',
413             help="Quantity of products that are planned to leave.\n"
414                  "In a context with a single Stock Location, this includes "
415                  "goods leaving this Location, or any of its children.\n"
416                  "In a context with a single Warehouse, this includes "
417                  "goods leaving the Stock Location of this Warehouse, or "
418                  "any of its children.\n"
419                  "In a context with a single Shop, this includes goods "
420                  "leaving the Stock Location of the Warehouse of this "
421                  "Shop, or any of its children.\n"
422                  "Otherwise, this includes goods leaving any Stock "
423                  "Location with 'internal' type."),
424         '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"),
425         '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"),
426         '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"),
427         'location_id': fields.dummy(string='Location', relation='stock.location', type='many2one'),
428         'warehouse_id': fields.dummy(string='Warehouse', relation='stock.warehouse', type='many2one'),
429         'valuation':fields.selection([('manual_periodic', 'Periodical (manual)'),
430                                         ('real_time','Real Time (automated)'),], 'Inventory Valuation',
431                                         help="If real-time valuation is enabled for a product, the system will automatically write journal entries corresponding to stock moves." \
432                                              "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."
433                                         , required=True),
434     }
435
436     _defaults = {
437         'valuation': 'manual_periodic',
438     }
439
440     def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
441         res = super(product_product,self).fields_view_get(cr, uid, view_id, view_type, context, toolbar=toolbar, submenu=submenu)
442         if context is None:
443             context = {}
444         if ('location' in context) and context['location']:
445             location_info = self.pool.get('stock.location').browse(cr, uid, context['location'])
446             fields=res.get('fields',{})
447             if fields:
448                 if location_info.usage == 'supplier':
449                     if fields.get('virtual_available'):
450                         res['fields']['virtual_available']['string'] = _('Future Receptions')
451                     if fields.get('qty_available'):
452                         res['fields']['qty_available']['string'] = _('Received Qty')
453
454                 if location_info.usage == 'internal':
455                     if fields.get('virtual_available'):
456                         res['fields']['virtual_available']['string'] = _('Future Stock')
457
458                 if location_info.usage == 'customer':
459                     if fields.get('virtual_available'):
460                         res['fields']['virtual_available']['string'] = _('Future Deliveries')
461                     if fields.get('qty_available'):
462                         res['fields']['qty_available']['string'] = _('Delivered Qty')
463
464                 if location_info.usage == 'inventory':
465                     if fields.get('virtual_available'):
466                         res['fields']['virtual_available']['string'] = _('Future P&L')
467                     if fields.get('qty_available'):
468                         res['fields']['qty_available']['string'] = _('P&L Qty')
469
470                 if location_info.usage == 'procurement':
471                     if fields.get('virtual_available'):
472                         res['fields']['virtual_available']['string'] = _('Future Qty')
473                     if fields.get('qty_available'):
474                         res['fields']['qty_available']['string'] = _('Unplanned Qty')
475
476                 if location_info.usage == 'production':
477                     if fields.get('virtual_available'):
478                         res['fields']['virtual_available']['string'] = _('Future Productions')
479                     if fields.get('qty_available'):
480                         res['fields']['qty_available']['string'] = _('Produced Qty')
481         return res
482
483 product_product()
484
485 class product_template(osv.osv):
486     _name = 'product.template'
487     _inherit = 'product.template'
488     _columns = {
489         'property_stock_procurement': fields.property(
490             'stock.location',
491             type='many2one',
492             relation='stock.location',
493             string="Procurement Location",
494             view_load=True,
495             domain=[('usage','like','procurement')],
496             help="This stock location will be used, instead of the default one, as the source location for stock moves generated by procurements."),
497         'property_stock_production': fields.property(
498             'stock.location',
499             type='many2one',
500             relation='stock.location',
501             string="Production Location",
502             view_load=True,
503             domain=[('usage','like','production')],
504             help="This stock location will be used, instead of the default one, as the source location for stock moves generated by manufacturing orders."),
505         'property_stock_inventory': fields.property(
506             'stock.location',
507             type='many2one',
508             relation='stock.location',
509             string="Inventory Location",
510             view_load=True,
511             domain=[('usage','like','inventory')],
512             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."),
513         'property_stock_account_input': fields.property('account.account',
514             type='many2one', relation='account.account',
515             string='Stock Input Account', view_load=True,
516             help="When doing real-time inventory valuation, counterpart journal items for all incoming stock moves will be posted in this account, unless "
517                  "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."),
518         'property_stock_account_output': fields.property('account.account',
519             type='many2one', relation='account.account',
520             string='Stock Output Account', view_load=True,
521             help="When doing real-time inventory valuation, counterpart journal items for all outgoing stock moves will be posted in this account, unless "
522                  "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."),
523         '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."),
524         'loc_rack': fields.char('Rack', size=16),
525         'loc_row': fields.char('Row', size=16),
526         'loc_case': fields.char('Case', size=16),
527     }
528
529     _defaults = {
530         'sale_delay': 7,
531     }
532 product_template()
533
534 class product_category(osv.osv):
535
536     _inherit = 'product.category'
537     _columns = {
538         'property_stock_journal': fields.property('account.journal',
539             relation='account.journal', type='many2one',
540             string='Stock Journal', view_load=True,
541             help="When doing real-time inventory valuation, this is the Accounting Journal in which entries will be automatically posted when stock moves are processed."),
542         'property_stock_account_input_categ': fields.property('account.account',
543             type='many2one', relation='account.account',
544             string='Stock Input Account', view_load=True,
545             help="When doing real-time inventory valuation, counterpart journal items for all incoming stock moves will be posted in this account, unless "
546                  "there is a specific valuation account set on the source location. This is the default value for all products in this category. It "
547                  "can also directly be set on each product"),
548         'property_stock_account_output_categ': fields.property('account.account',
549             type='many2one', relation='account.account',
550             string='Stock Output Account', view_load=True,
551             help="When doing real-time inventory valuation, counterpart journal items for all outgoing stock moves will be posted in this account, unless "
552                  "there is a specific valuation account set on the destination location. This is the default value for all products in this category. It "
553                  "can also directly be set on each product"),
554         'property_stock_valuation_account_id': fields.property('account.account',
555             type='many2one',
556             relation='account.account',
557             string="Stock Valuation Account",
558             view_load=True,
559             help="When real-time inventory valuation is enabled on a product, this account will hold the current value of the products.",),
560     }
561
562 product_category()
563
564 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: