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