[IMP] WIP removed 1000 lines of code
[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         context = context or {}
206
207         location_obj = self.pool.get('stock.location')
208         warehouse_obj = self.pool.get('stock.warehouse')
209
210         location_ids = []
211         if context.get('location', False):
212             if type(context['location']) == type(1):
213                 location_ids = [context['location']]
214             elif type(context['location']) in (type(''), type(u'')):
215                 domain = [('name','ilike',context['location'])]
216                 if context.get('force_company', False):
217                     domain += [('company_id', '=', context['force_company'])]
218                 location_ids = location_obj.search(cr, uid, domain, context=context)
219             else:
220                 location_ids = context['location']
221         else:
222             if context.get('warehouse', False):
223                 wh = warehouse_obj.browse(cr, uid, [context['warehouse']], context=context)
224             else:
225                 wids = warehouse_obj.search(cr, uid, [], context=context)
226                 wh = warehouse_obj.browse(cr, uid, wids, context=context)
227
228             for w in warehouse_obj.browse(cr, uid, wids, context=context):
229                 if not context.get('force_company', False) or w.lot_stock_id.company_id.id == context['force_company']:
230                     location_ids.append(w.lot_stock_id.id)
231
232         # build the list of ids of children of the location given by id
233         if context.get('compute_child',True):
234             if context.get('force_company', False):
235                 child_location_ids = location_obj.search(cr, uid, [('location_id', 'child_of', location_ids), ('company_id', '=', context['force_company'])])
236             else:
237                 child_location_ids = location_obj.search(cr, uid, [('location_id', 'child_of', location_ids)])
238             location_ids = child_location_ids or location_ids
239         return location_ids
240
241     def _get_date_query(self, cr, uid, ids, context):
242         '''
243             Parses the context and returns the dates query string needed to be processed in _get_product_available
244             It searches for a from_date and to_date
245         '''
246         from_date = context.get('from_date',False)
247         to_date = context.get('to_date',False)
248         date_str = False
249         whereadd = []
250         if from_date and to_date:
251             date_str = "date>=%s and date<=%s"
252             whereadd.append(tuple([from_date]))
253             whereadd.append(tuple([to_date]))
254         elif from_date:
255             date_str = "date>=%s"
256             whereadd.append(tuple([from_date]))
257         elif to_date:
258             date_str = "date<=%s"
259             whereadd.append(tuple([to_date]))
260         return (whereadd, date_str)
261
262     def get_product_available(self, cr, uid, ids, context=None):
263         """ Finds the quantity available of product(s) depending on parameters in the context
264         for what, states, locations (company, warehouse, ), date, lot, 
265         states: state of the move
266         what: in (dest in locations) or out (source in locations) moves
267         LOCATIONS:
268         shop: warehouse of the shop
269         warehouse: stock location of the warehouse
270         location: name (ilike) or id  of the location
271         force_company: if not warehouse or shop given: will only take from this company
272         compute_child (True if not specified): will also include child locations of locations above 
273             (when force_company only from that company)
274         
275         from_date and to_date: dates from or to for the date of the stock move to include (=scheduled of effective date)
276         prodlot: lot of the move
277         
278         @return: Dictionary of values for every product id
279         """
280         if context is None:
281             context = {}
282         
283         states = context.get('states',[])
284         what = context.get('what',())
285         if not ids:
286             ids = self.search(cr, uid, [])
287         res = {}.fromkeys(ids, 0.0)
288         if not ids:
289             return res
290         #set_context: refactor code here
291         location_ids = self._get_locations_from_context(cr, uid, ids, context=context)
292         if not location_ids: #in case of no locations, query will be empty anyways
293             return res
294         
295         # this will be a dictionary of the product UoM by product id
296         product2uom = {}
297         uom_ids = []
298         for product in self.read(cr, uid, ids, ['uom_id'], context=context):
299             product2uom[product['id']] = product['uom_id'][0]
300             uom_ids.append(product['uom_id'][0])
301         # this will be a dictionary of the UoM resources we need for conversion purposes, by UoM id
302         uoms_o = {}
303         for uom in self.pool.get('product.uom').browse(cr, uid, uom_ids, context=context):
304             uoms_o[uom.id] = uom
305
306         results = []
307         results2 = []
308
309         where = [tuple(location_ids),tuple(location_ids),tuple(ids),tuple(states)]
310
311         where_add, date_str = self._get_date_query(cr, uid, ids, context=context)
312         if where_add:
313             where += where_add
314
315
316         lot_id = context.get('lot_id', False)
317         prodlot_clause = ''
318         if lot_id:
319             prodlot_clause = ' and lot_id = %s '
320             where += [lot_id]
321
322         # TODO: perhaps merge in one query.
323         if 'in' in what:
324             # all moves from a location out of the set to a location in the set
325             cr.execute(
326                 'select sum(product_qty), product_id, product_uom '\
327                 'from stock_move '\
328                 'where location_id NOT IN %s '\
329                 'and location_dest_id IN %s '\
330                 'and product_id IN %s '\
331                 'and state IN %s ' + (date_str and 'and '+date_str+' ' or '') +' '\
332                 + prodlot_clause + 
333                 'group by product_id,product_uom',tuple(where))
334             results = cr.fetchall()
335         if 'out' in what:
336             # all moves from a location in the set to a location out of the set
337             cr.execute(
338                 'select sum(product_qty), product_id, product_uom '\
339                 'from stock_move '\
340                 'where location_id IN %s '\
341                 'and location_dest_id NOT 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             results2 = cr.fetchall()
347             
348         # Get the missing UoM resources
349         uom_obj = self.pool.get('product.uom')
350         uoms = map(lambda x: x[2], results) + map(lambda x: x[2], results2)
351         if context.get('uom', False):
352             uoms += [context['uom']]
353         uoms = filter(lambda x: x not in uoms_o.keys(), uoms)
354         if uoms:
355             uoms = uom_obj.browse(cr, uid, list(set(uoms)), context=context)
356             for o in uoms:
357                 uoms_o[o.id] = o
358                 
359         #TOCHECK: before change uom of product, stock move line are in old uom.
360         context.update({'raise-exception': False})
361         # Count the incoming quantities
362         for amount, prod_id, prod_uom in results:
363             amount = uom_obj._compute_qty_obj(cr, uid, uoms_o[prod_uom], amount,
364                      uoms_o[context.get('uom', False) or product2uom[prod_id]], context=context)
365             res[prod_id] += amount
366         # Count the outgoing quantities
367         for amount, prod_id, prod_uom in results2:
368             amount = uom_obj._compute_qty_obj(cr, uid, uoms_o[prod_uom], amount,
369                     uoms_o[context.get('uom', False) or product2uom[prod_id]], context=context)
370             res[prod_id] -= amount
371         return res
372
373     def _product_available(self, cr, uid, ids, field_names=None, arg=False, context=None):
374         """ Finds the incoming and outgoing quantity of product.
375         @return: Dictionary of values
376         """
377         if not field_names:
378             field_names = []
379         if context is None:
380             context = {}
381         res = {}
382         for id in ids:
383             res[id] = {}.fromkeys(field_names, 0.0)
384         for f in field_names:
385             c = context.copy()
386             if f == 'qty_available':
387                 c.update({ 'states': ('done',), 'what': ('in', 'out') })
388             if f == 'virtual_available':
389                 c.update({ 'states': ('confirmed','waiting','assigned','done'), 'what': ('in', 'out') })
390             if f == 'incoming_qty':
391                 c.update({ 'states': ('confirmed','waiting','assigned'), 'what': ('in',) })
392             if f == 'outgoing_qty':
393                 c.update({ 'states': ('confirmed','waiting','assigned'), 'what': ('out',) })
394             stock = self.get_product_available(cr, uid, ids, context=c)
395             for id in ids:
396                 res[id][f] = stock.get(id, 0.0)
397         return res
398
399     _columns = {
400         'reception_count': fields.function(_stock_move_count, string="Reception", type='integer', multi='pickings'),
401         'delivery_count': fields.function(_stock_move_count, string="Delivery", type='integer', multi='pickings'),
402         'qty_available': fields.function(_product_available, multi='qty_available',
403             type='float',  digits_compute=dp.get_precision('Product Unit of Measure'),
404             string='Quantity On Hand',
405             help="Current quantity of products.\n"
406                  "In a context with a single Stock Location, this includes "
407                  "goods stored at this Location, or any of its children.\n"
408                  "In a context with a single Warehouse, this includes "
409                  "goods stored in the Stock Location of this Warehouse, or any "
410                  "of its children.\n"
411                  "stored in the Stock Location of the Warehouse of this Shop, "
412                  "or any of its children.\n"
413                  "Otherwise, this includes goods stored in any Stock Location "
414                  "with 'internal' type."),
415         'virtual_available': fields.function(_product_available, multi='qty_available',
416             type='float',  digits_compute=dp.get_precision('Product Unit of Measure'),
417             string='Forecasted Quantity',
418             help="Forecast quantity (computed as Quantity On Hand "
419                  "- Outgoing + Incoming)\n"
420                  "In a context with a single Stock Location, this includes "
421                  "goods stored in this location, or any of its children.\n"
422                  "In a context with a single Warehouse, this includes "
423                  "goods stored in the Stock Location of this Warehouse, or any "
424                  "of its children.\n"
425                  "stored in the Stock Location of the Warehouse of this Shop, "
426                  "or any of its children.\n"
427                  "Otherwise, this includes goods stored in any Stock Location "
428                  "with 'internal' type."),
429         'incoming_qty': fields.function(_product_available, multi='qty_available',
430             type='float',  digits_compute=dp.get_precision('Product Unit of Measure'),
431             string='Incoming',
432             help="Quantity of products that are planned to arrive.\n"
433                  "In a context with a single Stock Location, this includes "
434                  "goods arriving to this Location, or any of its children.\n"
435                  "In a context with a single Warehouse, this includes "
436                  "goods arriving to the Stock Location of this Warehouse, or "
437                  "any of its children.\n"
438                  "In a context with a single Shop, this includes goods "
439                  "arriving to the Stock Location of the Warehouse of this "
440                  "Shop, or any of its children.\n"
441                  "Otherwise, this includes goods arriving to any Stock "
442                  "Location with 'internal' type."),
443         'outgoing_qty': fields.function(_product_available, multi='qty_available',
444             type='float',  digits_compute=dp.get_precision('Product Unit of Measure'),
445             string='Outgoing',
446             help="Quantity of products that are planned to leave.\n"
447                  "In a context with a single Stock Location, this includes "
448                  "goods leaving this Location, or any of its children.\n"
449                  "In a context with a single Warehouse, this includes "
450                  "goods leaving the Stock Location of this Warehouse, or "
451                  "any of its children.\n"
452                  "In a context with a single Shop, this includes goods "
453                  "leaving the Stock Location of the Warehouse of this "
454                  "Shop, or any of its children.\n"
455                  "Otherwise, this includes goods leaving any Stock "
456                  "Location with 'internal' type."),
457         '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"),
458         '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"),
459         '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"),
460         'location_id': fields.dummy(string='Location', relation='stock.location', type='many2one'),
461         'warehouse_id': fields.dummy(string='Warehouse', relation='stock.warehouse', type='many2one'),
462         'valuation':fields.property(type='selection', selection=[('manual_periodic', 'Periodical (manual)'),
463                                         ('real_time','Real Time (automated)'),], string = 'Inventory Valuation',
464                                         help="If real-time valuation is enabled for a product, the system will automatically write journal entries corresponding to stock moves." \
465                                              "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."
466                                         , required=True),
467     }
468
469     _defaults = {
470         'valuation': 'manual_periodic',
471     }
472
473     def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
474         res = super(product_product,self).fields_view_get(cr, uid, view_id, view_type, context, toolbar=toolbar, submenu=submenu)
475         if context is None:
476             context = {}
477         if ('location' in context) and context['location']:
478             location_info = self.pool.get('stock.location').browse(cr, uid, context['location'])
479             fields=res.get('fields',{})
480             if fields:
481                 if location_info.usage == 'supplier':
482                     if fields.get('virtual_available'):
483                         res['fields']['virtual_available']['string'] = _('Future Receptions')
484                     if fields.get('qty_available'):
485                         res['fields']['qty_available']['string'] = _('Received Qty')
486
487                 if location_info.usage == 'internal':
488                     if fields.get('virtual_available'):
489                         res['fields']['virtual_available']['string'] = _('Future Stock')
490
491                 if location_info.usage == 'customer':
492                     if fields.get('virtual_available'):
493                         res['fields']['virtual_available']['string'] = _('Future Deliveries')
494                     if fields.get('qty_available'):
495                         res['fields']['qty_available']['string'] = _('Delivered Qty')
496
497                 if location_info.usage == 'inventory':
498                     if fields.get('virtual_available'):
499                         res['fields']['virtual_available']['string'] = _('Future P&L')
500                     if fields.get('qty_available'):
501                         res['fields']['qty_available']['string'] = _('P&L Qty')
502
503                 if location_info.usage == 'procurement':
504                     if fields.get('virtual_available'):
505                         res['fields']['virtual_available']['string'] = _('Future Qty')
506                     if fields.get('qty_available'):
507                         res['fields']['qty_available']['string'] = _('Unplanned Qty')
508
509                 if location_info.usage == 'production':
510                     if fields.get('virtual_available'):
511                         res['fields']['virtual_available']['string'] = _('Future Productions')
512                     if fields.get('qty_available'):
513                         res['fields']['qty_available']['string'] = _('Produced Qty')
514         return res
515
516
517 class product_template(osv.osv):
518     _name = 'product.template'
519     _inherit = 'product.template'
520     _columns = {
521         'property_stock_procurement': fields.property(
522             type='many2one',
523             relation='stock.location',
524             string="Procurement Location",
525             domain=[('usage','like','procurement')],
526             help="This stock location will be used, instead of the default one, as the source location for stock moves generated by procurements."),
527         'property_stock_production': fields.property(
528             type='many2one',
529             relation='stock.location',
530             string="Production Location",
531             domain=[('usage','like','production')],
532             help="This stock location will be used, instead of the default one, as the source location for stock moves generated by manufacturing orders."),
533         'property_stock_inventory': fields.property(
534             type='many2one',
535             relation='stock.location',
536             string="Inventory Location",
537             domain=[('usage','like','inventory')],
538             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."),
539         'property_stock_account_input': fields.property(
540             type='many2one',
541             relation='account.account',
542             string='Stock Input Account',
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. When not set on the product, the one from the product category is used."),
545         'property_stock_account_output': fields.property(
546             type='many2one',
547             relation='account.account',
548             string='Stock Output Account',
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. When not set on the product, the one from the product category is used."),
551         '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."),
552         'loc_rack': fields.char('Rack', size=16),
553         'loc_row': fields.char('Row', size=16),
554         'loc_case': fields.char('Case', size=16),
555     }
556
557     _defaults = {
558         'sale_delay': 7,
559     }
560
561
562 # TODO: move this on stock module
563
564 class product_removal_strategy(osv.osv):
565     _name = 'product.removal'
566     _description = 'Removal Strategy'
567     _columns = {
568         'categ_ids':fields.one2many('product.category','removal_strategy_id', 'Product Categories', required=True),
569         'method': fields.selection([('fifo', 'FIFO'), ('lifo', 'LIFO')], "Method", required=True), 
570     }
571
572 class product_category(osv.osv):
573     _inherit = 'product.category'
574     _columns = {
575         'removal_strategy_id': fields.many2one('product.removal', 'Removal Strategy'),
576         'property_stock_journal': fields.property(
577             relation='account.journal',
578             type='many2one',
579             string='Stock Journal',
580             help="When doing real-time inventory valuation, this is the Accounting Journal in which entries will be automatically posted when stock moves are processed."),
581         'property_stock_account_input_categ': fields.property(
582             type='many2one',
583             relation='account.account',
584             string='Stock Input Account',
585             help="When doing real-time inventory valuation, counterpart journal items for all incoming stock moves will be posted in this account, unless "
586                  "there is a specific valuation account set on the source location. This is the default value for all products in this category. It "
587                  "can also directly be set on each product"),
588         'property_stock_account_output_categ': fields.property(
589             type='many2one',
590             relation='account.account',
591             string='Stock Output Account',
592             help="When doing real-time inventory valuation, counterpart journal items for all outgoing stock moves will be posted in this account, unless "
593                  "there is a specific valuation account set on the destination location. This is the default value for all products in this category. It "
594                  "can also directly be set on each product"),
595         'property_stock_valuation_account_id': fields.property(
596             type='many2one',
597             relation='account.account',
598             string="Stock Valuation Account",
599             help="When real-time inventory valuation is enabled on a product, this account will hold the current value of the products.",),
600     }
601
602
603 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: