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