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 _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',('draft','confirmed','assigned','pending'))
36 ], ['product_id'], ['product_id'])
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',('draft','confirmed','assigned','pending'))
44 ], ['product_id'], ['product_id'])
46 product_id = move['product_id'][0]
47 res[product_id]['reception_count'] = move['product_id_count']
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
57 product_obj = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
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
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
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
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
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
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')
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!'), _('Valuation Account is not specified for Product Category: %s') % (product_obj.categ_id.name))
98 loc_ids = location_obj.search(cr, uid,[('usage','=','internal')])
100 for location in location_obj.browse(cr, uid, loc_ids, context=context):
103 'location': location.id,
104 'compute_child': False
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!'), _("Could not find any difference between standard price and new price!"))
112 company_id = location.company_id and location.company_id.id or False
113 if not company_id: raise osv.except_osv(_('Error!'), _('Company is not specified in Location'))
118 journal_id = product.categ_id.property_stock_journal and product.categ_id.property_stock_journal.id or False
120 raise osv.except_osv(_('Error!'),
121 _('There is no journal defined '\
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
130 move_ids.append(move_id)
134 if not stock_input_acc:
135 stock_input_acc = product.product_tmpl_id.\
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 _('There is no stock input account defined ' \
143 'for this product: "%s" (id: %d)') % \
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,
153 move_line_obj.create(cr, uid, {
154 'name': product.categ_id.name,
155 'account_id': account_valuation_id,
156 'credit': amount_diff,
160 if not stock_output_acc:
161 stock_output_acc = product.product_tmpl_id.\
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 _('There is no stock output account defined ' \
169 'for this product: "%s" (id: %d)') % \
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,
179 move_line_obj.create(cr, uid, {
180 'name': product.categ_id.name,
181 'account_id': account_valuation_id,
182 'debit': amount_diff,
186 self.write(cr, uid, rec_id, {'standard_price': new_price})
190 def view_header_get(self, cr, user, view_id, view_type, context=None):
193 res = super(product_product, self).view_header_get(cr, user, view_id, view_type, context)
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
199 def get_product_available(self, cr, uid, ids, context=None):
200 """ Finds whether product is available or not in particular warehouse.
201 @return: Dictionary of values
206 location_obj = self.pool.get('stock.location')
207 warehouse_obj = self.pool.get('stock.warehouse')
208 shop_obj = self.pool.get('sale.shop')
210 states = context.get('states',[])
211 what = context.get('what',())
213 ids = self.search(cr, uid, [])
214 res = {}.fromkeys(ids, 0.0)
218 if context.get('shop', False):
219 warehouse_id = shop_obj.read(cr, uid, int(context['shop']), ['warehouse_id'])['warehouse_id'][0]
221 context['warehouse'] = warehouse_id
223 if context.get('warehouse', False):
224 lot_id = warehouse_obj.read(cr, uid, int(context['warehouse']), ['lot_stock_id'])['lot_stock_id'][0]
226 context['location'] = lot_id
228 if context.get('location', False):
229 if type(context['location']) == type(1):
230 location_ids = [context['location']]
231 elif type(context['location']) in (type(''), type(u'')):
232 location_ids = location_obj.search(cr, uid, [('name','ilike',context['location'])], context=context)
234 location_ids = context['location']
237 wids = warehouse_obj.search(cr, uid, [], context=context)
238 for w in warehouse_obj.browse(cr, uid, wids, context=context):
239 location_ids.append(w.lot_stock_id.id)
241 # build the list of ids of children of the location given by id
242 if context.get('compute_child',True):
243 child_location_ids = location_obj.search(cr, uid, [('location_id', 'child_of', location_ids)])
244 location_ids = child_location_ids or location_ids
246 # this will be a dictionary of the product UoM by product id
249 for product in self.read(cr, uid, ids, ['uom_id'], context=context):
250 product2uom[product['id']] = product['uom_id'][0]
251 uom_ids.append(product['uom_id'][0])
252 # this will be a dictionary of the UoM resources we need for conversion purposes, by UoM id
254 for uom in self.pool.get('product.uom').browse(cr, uid, uom_ids, context=context):
260 from_date = context.get('from_date',False)
261 to_date = context.get('to_date',False)
264 where = [tuple(location_ids),tuple(location_ids),tuple(ids),tuple(states)]
265 if from_date and to_date:
266 date_str = "date>=%s and date<=%s"
267 where.append(tuple([from_date]))
268 where.append(tuple([to_date]))
270 date_str = "date>=%s"
271 date_values = [from_date]
273 date_str = "date<=%s"
274 date_values = [to_date]
276 prodlot_id = context.get('prodlot_id', False)
278 # TODO: perhaps merge in one query.
280 where.append(tuple(date_values))
282 # all moves from a location out of the set to a location in the set
284 'select sum(product_qty), product_id, product_uom '\
286 'where location_id NOT IN %s '\
287 'and location_dest_id IN %s '\
288 'and product_id IN %s '\
289 '' + (prodlot_id and ('and prodlot_id = ' + str(prodlot_id)) or '') + ' '\
290 'and state IN %s ' + (date_str and 'and '+date_str+' ' or '') +' '\
291 'group by product_id,product_uom',tuple(where))
292 results = cr.fetchall()
294 # all moves from a location in the set to a location out of the set
296 'select sum(product_qty), product_id, product_uom '\
298 'where location_id IN %s '\
299 'and location_dest_id NOT IN %s '\
300 'and product_id IN %s '\
301 '' + (prodlot_id and ('and prodlot_id = ' + str(prodlot_id)) or '') + ' '\
302 'and state in %s ' + (date_str and 'and '+date_str+' ' or '') + ' '\
303 'group by product_id,product_uom',tuple(where))
304 results2 = cr.fetchall()
306 # Get the missing UoM resources
307 uom_obj = self.pool.get('product.uom')
308 uoms = map(lambda x: x[2], results) + map(lambda x: x[2], results2)
309 if context.get('uom', False):
310 uoms += [context['uom']]
311 uoms = filter(lambda x: x not in uoms_o.keys(), uoms)
313 uoms = uom_obj.browse(cr, uid, list(set(uoms)), context=context)
317 #TOCHECK: before change uom of product, stock move line are in old uom.
318 context.update({'raise-exception': False})
319 # Count the incoming quantities
320 for amount, prod_id, prod_uom in results:
321 amount = uom_obj._compute_qty_obj(cr, uid, uoms_o[prod_uom], amount,
322 uoms_o[context.get('uom', False) or product2uom[prod_id]], context=context)
323 res[prod_id] += amount
324 # Count the outgoing quantities
325 for amount, prod_id, prod_uom in results2:
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
331 def _product_available(self, cr, uid, ids, field_names=None, arg=False, context=None):
332 """ Finds the incoming and outgoing quantity of product.
333 @return: Dictionary of values
341 res[id] = {}.fromkeys(field_names, 0.0)
342 for f in field_names:
344 if f == 'qty_available':
345 c.update({ 'states': ('done',), 'what': ('in', 'out') })
346 if f == 'virtual_available':
347 c.update({ 'states': ('confirmed','waiting','assigned','done'), 'what': ('in', 'out') })
348 if f == 'incoming_qty':
349 c.update({ 'states': ('confirmed','waiting','assigned'), 'what': ('in',) })
350 if f == 'outgoing_qty':
351 c.update({ 'states': ('confirmed','waiting','assigned'), 'what': ('out',) })
352 stock = self.get_product_available(cr, uid, ids, context=c)
354 res[id][f] = stock.get(id, 0.0)
358 'reception_count': fields.function(_stock_move_count, string="Reception", type='integer', multi='pickings'),
359 'delivery_count': fields.function(_stock_move_count, string="Delivery", type='integer', multi='pickings'),
360 'qty_available': fields.function(_product_available, multi='qty_available',
361 type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
362 string='Quantity On Hand',
363 help="Current quantity of products.\n"
364 "In a context with a single Stock Location, this includes "
365 "goods stored at this Location, or any of its children.\n"
366 "In a context with a single Warehouse, this includes "
367 "goods stored in the Stock Location of this Warehouse, or any "
369 "In a context with a single Shop, this includes goods "
370 "stored in the Stock Location of the Warehouse of this Shop, "
371 "or any of its children.\n"
372 "Otherwise, this includes goods stored in any Stock Location "
373 "typed as 'internal'."),
374 'virtual_available': fields.function(_product_available, multi='qty_available',
375 type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
376 string='Quantity Available',
377 help="Forecast quantity (computed as Quantity On Hand "
378 "- Outgoing + Incoming)\n"
379 "In a context with a single Stock Location, this includes "
380 "goods stored at this Location, or any of its children.\n"
381 "In a context with a single Warehouse, this includes "
382 "goods stored in the Stock Location of this Warehouse, or any "
384 "In a context with a single Shop, this includes goods "
385 "stored in the Stock Location of the Warehouse of this Shop, "
386 "or any of its children.\n"
387 "Otherwise, this includes goods stored in any Stock Location "
388 "typed as 'internal'."),
389 'incoming_qty': fields.function(_product_available, multi='qty_available',
390 type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
392 help="Quantity of products that are planned to arrive.\n"
393 "In a context with a single Stock Location, this includes "
394 "goods arriving to this Location, or any of its children.\n"
395 "In a context with a single Warehouse, this includes "
396 "goods arriving to the Stock Location of this Warehouse, or "
397 "any of its children.\n"
398 "In a context with a single Shop, this includes goods "
399 "arriving to the Stock Location of the Warehouse of this "
400 "Shop, or any of its children.\n"
401 "Otherwise, this includes goods arriving to any Stock "
402 "Location typed as 'internal'."),
403 'outgoing_qty': fields.function(_product_available, multi='qty_available',
404 type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
406 help="Quantity of products that are planned to leave.\n"
407 "In a context with a single Stock Location, this includes "
408 "goods leaving from this Location, or any of its children.\n"
409 "In a context with a single Warehouse, this includes "
410 "goods leaving from the Stock Location of this Warehouse, or "
411 "any of its children.\n"
412 "In a context with a single Shop, this includes goods "
413 "leaving from the Stock Location of the Warehouse of this "
414 "Shop, or any of its children.\n"
415 "Otherwise, this includes goods leaving from any Stock "
416 "Location typed as 'internal'."),
417 '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"),
418 '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"),
419 '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"),
420 'location_id': fields.dummy(string='Location', relation='stock.location', type='many2one'),
421 'warehouse_id': fields.dummy(string='Warehouse', relation='stock.warehouse', type='many2one'),
422 'valuation':fields.selection([('manual_periodic', 'Periodical (manual)'),
423 ('real_time','Real Time (automated)'),], 'Inventory Valuation',
424 help="If real-time valuation is enabled for a product, the system will automatically write journal entries corresponding to stock moves." \
425 "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."
430 'valuation': 'manual_periodic',
433 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
434 res = super(product_product,self).fields_view_get(cr, uid, view_id, view_type, context, toolbar=toolbar, submenu=submenu)
437 if ('location' in context) and context['location']:
438 location_info = self.pool.get('stock.location').browse(cr, uid, context['location'])
439 fields=res.get('fields',{})
441 if location_info.usage == 'supplier':
442 if fields.get('virtual_available'):
443 res['fields']['virtual_available']['string'] = _('Future Receptions')
444 if fields.get('qty_available'):
445 res['fields']['qty_available']['string'] = _('Received Qty')
447 if location_info.usage == 'internal':
448 if fields.get('virtual_available'):
449 res['fields']['virtual_available']['string'] = _('Future Stock')
451 if location_info.usage == 'customer':
452 if fields.get('virtual_available'):
453 res['fields']['virtual_available']['string'] = _('Future Deliveries')
454 if fields.get('qty_available'):
455 res['fields']['qty_available']['string'] = _('Delivered Qty')
457 if location_info.usage == 'inventory':
458 if fields.get('virtual_available'):
459 res['fields']['virtual_available']['string'] = _('Future P&L')
460 if fields.get('qty_available'):
461 res['fields']['qty_available']['string'] = _('P&L Qty')
463 if location_info.usage == 'procurement':
464 if fields.get('virtual_available'):
465 res['fields']['virtual_available']['string'] = _('Future Qty')
466 if fields.get('qty_available'):
467 res['fields']['qty_available']['string'] = _('Unplanned Qty')
469 if location_info.usage == 'production':
470 if fields.get('virtual_available'):
471 res['fields']['virtual_available']['string'] = _('Future Productions')
472 if fields.get('qty_available'):
473 res['fields']['qty_available']['string'] = _('Produced Qty')
478 class product_template(osv.osv):
479 _name = 'product.template'
480 _inherit = 'product.template'
482 'property_stock_procurement': fields.property(
485 relation='stock.location',
486 string="Procurement Location",
488 domain=[('usage','like','procurement')],
489 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"),
490 'property_stock_production': fields.property(
493 relation='stock.location',
494 string="Production Location",
496 domain=[('usage','like','production')],
497 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"),
498 'property_stock_inventory': fields.property(
501 relation='stock.location',
502 string="Inventory Location",
504 domain=[('usage','like','inventory')],
505 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"),
506 'property_stock_account_input': fields.property('account.account',
507 type='many2one', relation='account.account',
508 string='Stock Input Account', view_load=True,
509 help="When doing real-time inventory valuation, counterpart journal items for all incoming stock moves will be posted in this account, unless "
510 "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."),
511 'property_stock_account_output': fields.property('account.account',
512 type='many2one', relation='account.account',
513 string='Stock Output Account', view_load=True,
514 help="When doing real-time inventory valuation, counterpart journal items for all outgoing stock moves will be posted in this account, unless "
515 "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."),
520 class product_category(osv.osv):
522 _inherit = 'product.category'
524 'property_stock_journal': fields.property('account.journal',
525 relation='account.journal', type='many2one',
526 string='Stock journal', view_load=True,
527 help="When doing real-time inventory valuation, this is the Accounting Journal in which entries will be automatically posted when stock moves are processed."),
528 'property_stock_account_input_categ': fields.property('account.account',
529 type='many2one', relation='account.account',
530 string='Stock Input Account', view_load=True,
531 help="When doing real-time inventory valuation, counterpart journal items for all incoming stock moves will be posted in this account, unless "
532 "there is a specific valuation account set on the source location. This is the default value for all products in this category. It "
533 "can also directly be set on each product"),
534 'property_stock_account_output_categ': fields.property('account.account',
535 type='many2one', relation='account.account',
536 string='Stock Output Account', view_load=True,
537 help="When doing real-time inventory valuation, counterpart journal items for all outgoing stock moves will be posted in this account, unless "
538 "there is a specific valuation account set on the destination location. This is the default value for all products in this category. It "
539 "can also directly be set on each product"),
540 'property_stock_valuation_account_id': fields.property('account.account',
542 relation='account.account',
543 string="Stock Valuation Account",
545 help="When real-time inventory valuation is enabled on a product, this account will hold the current value of the products.",),
550 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: