1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
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.
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.
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/>.
20 ##############################################################################
22 from osv import fields, osv
23 from tools.translate import _
24 import decimal_precision as dp
26 class product_product(osv.osv):
27 _inherit = "product.product"
29 def get_product_accounts(self, cr, uid, product_id, context=None):
30 """ To get the stock input account, stock output account and stock journal related to product.
31 @param product_id: product id
32 @return: dictionary which contains information regarding stock input account, stock output account and stock journal
36 product_obj = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
38 stock_input_acc = product_obj.property_stock_account_input and product_obj.property_stock_account_input.id or False
39 if not stock_input_acc:
40 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
42 stock_output_acc = product_obj.property_stock_account_output and product_obj.property_stock_account_output.id or False
43 if not stock_output_acc:
44 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
46 journal_id = product_obj.categ_id.property_stock_journal and product_obj.categ_id.property_stock_journal.id or False
47 account_valuation = product_obj.categ_id.property_stock_valuation_account_id and product_obj.categ_id.property_stock_valuation_account_id.id or False
49 'stock_account_input': stock_input_acc,
50 'stock_account_output': stock_output_acc,
51 'stock_journal': journal_id,
52 'property_stock_valuation_account_id': account_valuation
55 def do_change_standard_price(self, cr, uid, ids, datas, context=None):
56 """ Changes the Standard Price of Product and creates an account move accordingly.
57 @param datas : dict. contain default datas like new_price, stock_output_account, stock_input_account, stock_journal
58 @param context: A standard dictionary
62 location_obj = self.pool.get('stock.location')
63 move_obj = self.pool.get('account.move')
64 move_line_obj = self.pool.get('account.move.line')
68 new_price = datas.get('new_price', 0.0)
69 stock_output_acc = datas.get('stock_output_account', False)
70 stock_input_acc = datas.get('stock_input_account', False)
71 journal_id = datas.get('stock_journal', False)
72 product_obj=self.browse(cr, uid, ids, context=context)[0]
73 account_valuation = product_obj.categ_id.property_stock_valuation_account_id
74 account_valuation_id = account_valuation and account_valuation.id or False
75 if not account_valuation_id: raise osv.except_osv(_('Error!'), _('Valuation Account is not specified for Product Category: %s') % (product_obj.categ_id.name))
77 loc_ids = location_obj.search(cr, uid,[('usage','=','internal')])
79 for location in location_obj.browse(cr, uid, loc_ids, context=context):
82 'location': location.id,
83 'compute_child': False
86 product = self.browse(cr, uid, rec_id, context=c)
87 qty = product.qty_available
88 diff = product.standard_price - new_price
89 if not diff: raise osv.except_osv(_('Error!'), _("Could not find any difference between standard price and new price!"))
91 company_id = location.company_id and location.company_id.id or False
92 if not company_id: raise osv.except_osv(_('Error!'), _('Company is not specified in Location'))
97 journal_id = product.categ_id.property_stock_journal and product.categ_id.property_stock_journal.id or False
99 raise osv.except_osv(_('Error!'),
100 _('There is no journal defined '\
101 'on the product category: "%s" (id: %d)') % \
102 (product.categ_id.name,
103 product.categ_id.id,))
104 move_id = move_obj.create(cr, uid, {
105 'journal_id': journal_id,
106 'company_id': company_id
109 move_ids.append(move_id)
113 if not stock_input_acc:
114 stock_input_acc = product.product_tmpl_id.\
115 property_stock_account_input.id
116 if not stock_input_acc:
117 stock_input_acc = product.categ_id.\
118 property_stock_account_input_categ.id
119 if not stock_input_acc:
120 raise osv.except_osv(_('Error!'),
121 _('There is no stock input account defined ' \
122 'for this product: "%s" (id: %d)') % \
125 amount_diff = qty * diff
126 move_line_obj.create(cr, uid, {
127 'name': product.name,
128 'account_id': stock_input_acc,
129 'debit': amount_diff,
132 move_line_obj.create(cr, uid, {
133 'name': product.categ_id.name,
134 'account_id': account_valuation_id,
135 'credit': amount_diff,
139 if not stock_output_acc:
140 stock_output_acc = product.product_tmpl_id.\
141 property_stock_account_output.id
142 if not stock_output_acc:
143 stock_output_acc = product.categ_id.\
144 property_stock_account_output_categ.id
145 if not stock_output_acc:
146 raise osv.except_osv(_('Error!'),
147 _('There is no stock output account defined ' \
148 'for this product: "%s" (id: %d)') % \
151 amount_diff = qty * -diff
152 move_line_obj.create(cr, uid, {
153 'name': product.name,
154 'account_id': stock_output_acc,
155 'credit': amount_diff,
158 move_line_obj.create(cr, uid, {
159 'name': product.categ_id.name,
160 'account_id': account_valuation_id,
161 'debit': amount_diff,
165 self.write(cr, uid, rec_id, {'standard_price': new_price})
169 def view_header_get(self, cr, user, view_id, view_type, context=None):
172 res = super(product_product, self).view_header_get(cr, user, view_id, view_type, context)
174 if (context.get('active_id', False)) and (context.get('active_model') == 'stock.location'):
175 return _('Products: ')+self.pool.get('stock.location').browse(cr, user, context['active_id'], context).name
178 def get_product_available(self, cr, uid, ids, context=None):
179 """ Finds whether product is available or not in particular warehouse.
180 @return: Dictionary of values
185 location_obj = self.pool.get('stock.location')
186 warehouse_obj = self.pool.get('stock.warehouse')
187 shop_obj = self.pool.get('sale.shop')
189 states = context.get('states',[])
190 what = context.get('what',())
192 ids = self.search(cr, uid, [])
193 res = {}.fromkeys(ids, 0.0)
197 if context.get('shop', False):
198 warehouse_id = shop_obj.read(cr, uid, int(context['shop']), ['warehouse_id'])['warehouse_id'][0]
200 context['warehouse'] = warehouse_id
202 if context.get('warehouse', False):
203 lot_id = warehouse_obj.read(cr, uid, int(context['warehouse']), ['lot_stock_id'])['lot_stock_id'][0]
205 context['location'] = lot_id
207 if context.get('location', False):
208 if type(context['location']) == type(1):
209 location_ids = [context['location']]
210 elif type(context['location']) in (type(''), type(u'')):
211 location_ids = location_obj.search(cr, uid, [('name','ilike',context['location'])], context=context)
213 location_ids = context['location']
216 wids = warehouse_obj.search(cr, uid, [], context=context)
217 for w in warehouse_obj.browse(cr, uid, wids, context=context):
218 location_ids.append(w.lot_stock_id.id)
220 # build the list of ids of children of the location given by id
221 if context.get('compute_child',True):
222 child_location_ids = location_obj.search(cr, uid, [('location_id', 'child_of', location_ids)])
223 location_ids = child_location_ids or location_ids
225 # this will be a dictionary of the UoM resources we need for conversion purposes, by UoM id
227 # this will be a dictionary of the product UoM by product id
229 for product in self.browse(cr, uid, ids, context=context):
230 product2uom[product.id] = product.uom_id.id
231 uoms_o[product.uom_id.id] = product.uom_id
236 from_date = context.get('from_date',False)
237 to_date = context.get('to_date',False)
240 where = [tuple(location_ids),tuple(location_ids),tuple(ids),tuple(states)]
241 if from_date and to_date:
242 date_str = "date>=%s and date<=%s"
243 where.append(tuple([from_date]))
244 where.append(tuple([to_date]))
246 date_str = "date>=%s"
247 date_values = [from_date]
249 date_str = "date<=%s"
250 date_values = [to_date]
252 prodlot_id = context.get('prodlot_id', False)
254 # TODO: perhaps merge in one query.
256 where.append(tuple(date_values))
258 # all moves from a location out of the set to a location in the set
260 'select sum(product_qty), product_id, product_uom '\
262 'where location_id NOT IN %s '\
263 'and location_dest_id IN %s '\
264 'and product_id IN %s '\
265 '' + (prodlot_id and ('and prodlot_id = ' + str(prodlot_id)) or '') + ' '\
266 'and state IN %s ' + (date_str and 'and '+date_str+' ' or '') +' '\
267 'group by product_id,product_uom',tuple(where))
268 results = cr.fetchall()
270 # all moves from a location in the set to a location out of the set
272 'select sum(product_qty), product_id, product_uom '\
274 'where location_id IN %s '\
275 'and location_dest_id NOT IN %s '\
276 'and product_id IN %s '\
277 '' + (prodlot_id and ('and prodlot_id = ' + str(prodlot_id)) or '') + ' '\
278 'and state in %s ' + (date_str and 'and '+date_str+' ' or '') + ' '\
279 'group by product_id,product_uom',tuple(where))
280 results2 = cr.fetchall()
282 # Get the missing UoM resources
283 uom_obj = self.pool.get('product.uom')
284 uoms = map(lambda x: x[2], results) + map(lambda x: x[2], results2)
285 if context.get('uom', False):
286 uoms += [context['uom']]
287 uoms = filter(lambda x: x not in uoms_o.keys(), uoms)
289 uoms = uom_obj.browse(cr, uid, list(set(uoms)), context=context)
293 #TOCHECK: before change uom of product, stock move line are in old uom.
294 context.update({'raise-exception': False})
295 # Count the incoming quantities
296 for amount, prod_id, prod_uom in results:
297 amount = uom_obj._compute_qty_obj(cr, uid, uoms_o[prod_uom], amount,
298 uoms_o[context.get('uom', False) or product2uom[prod_id]], context=context)
299 res[prod_id] += amount
300 # Count the outgoing quantities
301 for amount, prod_id, prod_uom in results2:
302 amount = uom_obj._compute_qty_obj(cr, uid, uoms_o[prod_uom], amount,
303 uoms_o[context.get('uom', False) or product2uom[prod_id]], context=context)
304 res[prod_id] -= amount
307 def _product_available(self, cr, uid, ids, field_names=None, arg=False, context=None):
308 """ Finds the incoming and outgoing quantity of product.
309 @return: Dictionary of values
317 res[id] = {}.fromkeys(field_names, 0.0)
318 for f in field_names:
320 if f == 'qty_available':
321 c.update({ 'states': ('done',), 'what': ('in', 'out') })
322 if f == 'virtual_available':
323 c.update({ 'states': ('confirmed','waiting','assigned','done'), 'what': ('in', 'out') })
324 if f == 'incoming_qty':
325 c.update({ 'states': ('confirmed','waiting','assigned'), 'what': ('in',) })
326 if f == 'outgoing_qty':
327 c.update({ 'states': ('confirmed','waiting','assigned'), 'what': ('out',) })
328 stock = self.get_product_available(cr, uid, ids, context=c)
330 res[id][f] = stock.get(id, 0.0)
334 'qty_available': fields.function(_product_available, multi='qty_available',
335 type='float', digits_compute=dp.get_precision('Product UoM'),
336 string='Quantity On Hand',
337 help="Current quantity of products.\n"
338 "In a context with a single Stock Location, this includes "
339 "goods stored at this Location, or any of its children.\n"
340 "In a context with a single Warehouse, this includes "
341 "goods stored in the Stock Location of this Warehouse, or any "
343 "In a context with a single Shop, this includes goods "
344 "stored in the Stock Location of the Warehouse of this Shop, "
345 "or any of its children.\n"
346 "Otherwise, this includes goods stored in any Stock Location "
347 "typed as 'internal'."),
348 'virtual_available': fields.function(_product_available, multi='qty_available',
349 type='float', digits_compute=dp.get_precision('Product UoM'),
350 string='Quantity Available',
351 help="Forecast quantity (computed as Quantity On Hand "
352 "- Outgoing + Incoming)\n"
353 "In a context with a single Stock Location, this includes "
354 "goods stored at this Location, or any of its children.\n"
355 "In a context with a single Warehouse, this includes "
356 "goods stored in the Stock Location of this Warehouse, or any "
358 "In a context with a single Shop, this includes goods "
359 "stored in the Stock Location of the Warehouse of this Shop, "
360 "or any of its children.\n"
361 "Otherwise, this includes goods stored in any Stock Location "
362 "typed as 'internal'."),
363 'incoming_qty': fields.function(_product_available, multi='qty_available',
364 type='float', digits_compute=dp.get_precision('Product UoM'),
366 help="Quantity of products that are planned to arrive.\n"
367 "In a context with a single Stock Location, this includes "
368 "goods arriving to this Location, or any of its children.\n"
369 "In a context with a single Warehouse, this includes "
370 "goods arriving to the Stock Location of this Warehouse, or "
371 "any of its children.\n"
372 "In a context with a single Shop, this includes goods "
373 "arriving to the Stock Location of the Warehouse of this "
374 "Shop, or any of its children.\n"
375 "Otherwise, this includes goods arriving to any Stock "
376 "Location typed as 'internal'."),
377 'outgoing_qty': fields.function(_product_available, multi='qty_available',
378 type='float', digits_compute=dp.get_precision('Product UoM'),
380 help="Quantity of products that are planned to leave.\n"
381 "In a context with a single Stock Location, this includes "
382 "goods leaving from this Location, or any of its children.\n"
383 "In a context with a single Warehouse, this includes "
384 "goods leaving from the Stock Location of this Warehouse, or "
385 "any of its children.\n"
386 "In a context with a single Shop, this includes goods "
387 "leaving from the Stock Location of the Warehouse of this "
388 "Shop, or any of its children.\n"
389 "Otherwise, this includes goods leaving from any Stock "
390 "Location typed as 'internal'."),
391 'track_production': fields.boolean('Track Manufacturing Lots', help="Forces to specify a Production Lot for all moves containing this product and generated by a Manufacturing Order"),
392 'track_incoming': fields.boolean('Track Incoming Lots', help="Forces to specify a Production Lot for all moves containing this product and coming from a Supplier Location"),
393 'track_outgoing': fields.boolean('Track Outgoing Lots', help="Forces to specify a Production Lot for all moves containing this product and going to a Customer Location"),
394 'location_id': fields.dummy(string='Location', relation='stock.location', type='many2one'),
395 'warehouse_id': fields.dummy(string='Warehouse', relation='stock.warehouse', type='many2one'),
396 'valuation':fields.selection([('manual_periodic', 'Periodical (manual)'),
397 ('real_time','Real Time (automated)'),], 'Inventory Valuation',
398 help="If real-time valuation is enabled for a product, the system will automatically write journal entries corresponding to stock moves." \
399 "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."
404 'valuation': 'manual_periodic',
407 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
408 res = super(product_product,self).fields_view_get(cr, uid, view_id, view_type, context, toolbar=toolbar, submenu=submenu)
411 if ('location' in context) and context['location']:
412 location_info = self.pool.get('stock.location').browse(cr, uid, context['location'])
413 fields=res.get('fields',{})
415 if location_info.usage == 'supplier':
416 if fields.get('virtual_available'):
417 res['fields']['virtual_available']['string'] = _('Future Receptions')
418 if fields.get('qty_available'):
419 res['fields']['qty_available']['string'] = _('Received Qty')
421 if location_info.usage == 'internal':
422 if fields.get('virtual_available'):
423 res['fields']['virtual_available']['string'] = _('Future Stock')
425 if location_info.usage == 'customer':
426 if fields.get('virtual_available'):
427 res['fields']['virtual_available']['string'] = _('Future Deliveries')
428 if fields.get('qty_available'):
429 res['fields']['qty_available']['string'] = _('Delivered Qty')
431 if location_info.usage == 'inventory':
432 if fields.get('virtual_available'):
433 res['fields']['virtual_available']['string'] = _('Future P&L')
434 if fields.get('qty_available'):
435 res['fields']['qty_available']['string'] = _('P&L Qty')
437 if location_info.usage == 'procurement':
438 if fields.get('virtual_available'):
439 res['fields']['virtual_available']['string'] = _('Future Qty')
440 if fields.get('qty_available'):
441 res['fields']['qty_available']['string'] = _('Unplanned Qty')
443 if location_info.usage == 'production':
444 if fields.get('virtual_available'):
445 res['fields']['virtual_available']['string'] = _('Future Productions')
446 if fields.get('qty_available'):
447 res['fields']['qty_available']['string'] = _('Produced Qty')
452 class product_template(osv.osv):
453 _name = 'product.template'
454 _inherit = 'product.template'
456 'property_stock_procurement': fields.property(
459 relation='stock.location',
460 string="Procurement Location",
462 domain=[('usage','like','procurement')],
463 help="For the current product, this stock location will be used, instead of the default one, as the source location for stock moves generated by procurements"),
464 'property_stock_production': fields.property(
467 relation='stock.location',
468 string="Production Location",
470 domain=[('usage','like','production')],
471 help="For the current product, this stock location will be used, instead of the default one, as the source location for stock moves generated by production orders"),
472 'property_stock_inventory': fields.property(
475 relation='stock.location',
476 string="Inventory Location",
478 domain=[('usage','like','inventory')],
479 help="For the current product, this stock location will be used, instead of the default one, as the source location for stock moves generated when you do an inventory"),
480 'property_stock_account_input': fields.property('account.account',
481 type='many2one', relation='account.account',
482 string='Stock Input Account', view_load=True,
483 help="When doing real-time inventory valuation, counterpart journal items for all incoming stock moves will be posted in this account, unless "
484 "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."),
485 'property_stock_account_output': fields.property('account.account',
486 type='many2one', relation='account.account',
487 string='Stock Output Account', view_load=True,
488 help="When doing real-time inventory valuation, counterpart journal items for all outgoing stock moves will be posted in this account, unless "
489 "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."),
494 class product_category(osv.osv):
496 _inherit = 'product.category'
498 'property_stock_journal': fields.property('account.journal',
499 relation='account.journal', type='many2one',
500 string='Stock journal', view_load=True,
501 help="When doing real-time inventory valuation, this is the Accounting Journal in which entries will be automatically posted when stock moves are processed."),
502 'property_stock_account_input_categ': fields.property('account.account',
503 type='many2one', relation='account.account',
504 string='Stock Input Account', view_load=True,
505 help="When doing real-time inventory valuation, counterpart journal items for all incoming stock moves will be posted in this account, unless "
506 "there is a specific valuation account set on the source location. This is the default value for all products in this category. It "
507 "can also directly be set on each product"),
508 'property_stock_account_output_categ': fields.property('account.account',
509 type='many2one', relation='account.account',
510 string='Stock Output Account', view_load=True,
511 help="When doing real-time inventory valuation, counterpart journal items for all outgoing stock moves will be posted in this account, unless "
512 "there is a specific valuation account set on the destination location. This is the default value for all products in this category. It "
513 "can also directly be set on each product"),
514 'property_stock_valuation_account_id': fields.property('account.account',
516 relation='account.account',
517 string="Stock Valuation Account",
519 help="When real-time inventory valuation is enabled on a product, this account will hold the current value of the products.",),
524 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: