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 openerp.osv import fields, osv
23 from openerp.tools.translate import _
24 import openerp.addons.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',('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',('confirmed','assigned','pending'))
44 ], ['product_id'], ['product_id'])
46 product_id = move['product_id'][0]
47 res[product_id]['delivery_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)
94 loc_ids = location_obj.search(cr, uid,[('usage','=','internal')])
95 for product in self.browse(cr, uid, ids, context=context):
96 if product.valuation != 'real_time':
98 account_valuation = product.categ_id.property_stock_valuation_account_id
99 account_valuation_id = account_valuation and account_valuation.id or False
100 if not account_valuation_id: raise osv.except_osv(_('Error!'), _('Specify valuation Account for Product Category: %s.') % (product.categ_id.name))
101 for location in location_obj.browse(cr, uid, loc_ids, context=context):
104 'location': location.id,
105 'compute_child': False
108 # qty_available depends of the location in the context
109 qty = self.read(cr, uid, [product.id], ['qty_available'], context=c)[0]['qty_available']
111 diff = product.standard_price - new_price
112 if not diff: raise osv.except_osv(_('Error!'), _("No difference between standard price and new price!"))
114 company_id = location.company_id and location.company_id.id or False
115 if not company_id: raise osv.except_osv(_('Error!'), _('Please specify company in Location.'))
120 journal_id = product.categ_id.property_stock_journal and product.categ_id.property_stock_journal.id or False
122 raise osv.except_osv(_('Error!'),
123 _('Please define journal '\
124 'on the product category: "%s" (id: %d).') % \
125 (product.categ_id.name,
126 product.categ_id.id,))
127 move_id = move_obj.create(cr, uid, {
128 'journal_id': journal_id,
129 'company_id': company_id
132 move_ids.append(move_id)
136 if not stock_input_acc:
137 stock_input_acc = product.\
138 property_stock_account_input.id
139 if not stock_input_acc:
140 stock_input_acc = product.categ_id.\
141 property_stock_account_input_categ.id
142 if not stock_input_acc:
143 raise osv.except_osv(_('Error!'),
144 _('Please define stock input account ' \
145 'for this product: "%s" (id: %d).') % \
148 amount_diff = qty * diff
149 move_line_obj.create(cr, uid, {
150 'name': product.name,
151 'account_id': stock_input_acc,
152 'debit': amount_diff,
155 move_line_obj.create(cr, uid, {
156 'name': product.categ_id.name,
157 'account_id': account_valuation_id,
158 'credit': amount_diff,
162 if not stock_output_acc:
163 stock_output_acc = product.\
164 property_stock_account_output.id
165 if not stock_output_acc:
166 stock_output_acc = product.categ_id.\
167 property_stock_account_output_categ.id
168 if not stock_output_acc:
169 raise osv.except_osv(_('Error!'),
170 _('Please define stock output account ' \
171 'for this product: "%s" (id: %d).') % \
174 amount_diff = qty * -diff
175 move_line_obj.create(cr, uid, {
176 'name': product.name,
177 'account_id': stock_output_acc,
178 'credit': amount_diff,
181 move_line_obj.create(cr, uid, {
182 'name': product.categ_id.name,
183 'account_id': account_valuation_id,
184 'debit': amount_diff,
187 self.write(cr, uid, ids, {'standard_price': new_price})
191 def view_header_get(self, cr, user, view_id, view_type, context=None):
194 res = super(product_product, self).view_header_get(cr, user, view_id, view_type, context)
196 if (context.get('active_id', False)) and (context.get('active_model') == 'stock.location'):
197 return _('Products: ')+self.pool.get('stock.location').browse(cr, user, context['active_id'], context).name
200 def get_product_available(self, cr, uid, ids, context=None):
201 """ Finds whether product is available or not in particular warehouse.
202 @return: Dictionary of values
207 location_obj = self.pool.get('stock.location')
208 warehouse_obj = self.pool.get('stock.warehouse')
209 shop_obj = self.pool.get('sale.shop')
211 states = context.get('states',[])
212 what = context.get('what',())
214 ids = self.search(cr, uid, [])
215 res = {}.fromkeys(ids, 0.0)
219 if context.get('shop', False):
220 warehouse_id = shop_obj.read(cr, uid, int(context['shop']), ['warehouse_id'])['warehouse_id'][0]
222 context['warehouse'] = warehouse_id
224 if context.get('warehouse', False):
225 lot_id = warehouse_obj.read(cr, uid, int(context['warehouse']), ['lot_stock_id'])['lot_stock_id'][0]
227 context['location'] = lot_id
229 if context.get('location', False):
230 if type(context['location']) == type(1):
231 location_ids = [context['location']]
232 elif type(context['location']) in (type(''), type(u'')):
233 location_ids = location_obj.search(cr, uid, [('name','ilike',context['location'])], context=context)
235 location_ids = context['location']
238 wids = warehouse_obj.search(cr, uid, [], context=context)
241 for w in warehouse_obj.browse(cr, uid, wids, context=context):
242 location_ids.append(w.lot_stock_id.id)
244 # build the list of ids of children of the location given by id
245 if context.get('compute_child',True):
246 child_location_ids = location_obj.search(cr, uid, [('location_id', 'child_of', location_ids)])
247 location_ids = child_location_ids or location_ids
249 # this will be a dictionary of the product UoM by product id
252 for product in self.read(cr, uid, ids, ['uom_id'], context=context):
253 product2uom[product['id']] = product['uom_id'][0]
254 uom_ids.append(product['uom_id'][0])
255 # this will be a dictionary of the UoM resources we need for conversion purposes, by UoM id
257 for uom in self.pool.get('product.uom').browse(cr, uid, uom_ids, context=context):
263 from_date = context.get('from_date',False)
264 to_date = context.get('to_date',False)
267 where = [tuple(location_ids),tuple(location_ids),tuple(ids),tuple(states)]
268 if from_date and to_date:
269 date_str = "date>=%s and date<=%s"
270 where.append(tuple([from_date]))
271 where.append(tuple([to_date]))
273 date_str = "date>=%s"
274 date_values = [from_date]
276 date_str = "date<=%s"
277 date_values = [to_date]
279 where.append(tuple(date_values))
281 prodlot_id = context.get('prodlot_id', False)
284 prodlot_clause = ' and prodlot_id = %s '
285 where += [prodlot_id]
286 elif 'prodlot_id' in context and not prodlot_id:
287 prodlot_clause = ' and prodlot_id is null '
289 # TODO: perhaps merge in one query.
291 # all moves from a location out of the set to a location in the set
293 'select sum(product_qty), product_id, product_uom '\
295 'where location_id NOT IN %s '\
296 'and location_dest_id IN %s '\
297 'and product_id IN %s '\
298 'and state IN %s ' + (date_str and 'and '+date_str+' ' or '') +' '\
300 'group by product_id,product_uom',tuple(where))
301 results = cr.fetchall()
303 # all moves from a location in the set to a location out of the set
305 'select sum(product_qty), product_id, product_uom '\
307 'where location_id IN %s '\
308 'and location_dest_id NOT IN %s '\
309 'and product_id IN %s '\
310 'and state in %s ' + (date_str and 'and '+date_str+' ' or '') + ' '\
312 'group by product_id,product_uom',tuple(where))
313 results2 = cr.fetchall()
315 # Get the missing UoM resources
316 uom_obj = self.pool.get('product.uom')
317 uoms = map(lambda x: x[2], results) + map(lambda x: x[2], results2)
318 if context.get('uom', False):
319 uoms += [context['uom']]
320 uoms = filter(lambda x: x not in uoms_o.keys(), uoms)
322 uoms = uom_obj.browse(cr, uid, list(set(uoms)), context=context)
326 #TOCHECK: before change uom of product, stock move line are in old uom.
327 context.update({'raise-exception': False})
328 # Count the incoming quantities
329 for amount, prod_id, prod_uom in results:
330 amount = uom_obj._compute_qty_obj(cr, uid, uoms_o[prod_uom], amount,
331 uoms_o[context.get('uom', False) or product2uom[prod_id]], context=context)
332 res[prod_id] += amount
333 # Count the outgoing quantities
334 for amount, prod_id, prod_uom in results2:
335 amount = uom_obj._compute_qty_obj(cr, uid, uoms_o[prod_uom], amount,
336 uoms_o[context.get('uom', False) or product2uom[prod_id]], context=context)
337 res[prod_id] -= amount
340 def _product_available(self, cr, uid, ids, field_names=None, arg=False, context=None):
341 """ Finds the incoming and outgoing quantity of product.
342 @return: Dictionary of values
350 res[id] = {}.fromkeys(field_names, 0.0)
351 for f in field_names:
353 if f == 'qty_available':
354 c.update({ 'states': ('done',), 'what': ('in', 'out') })
355 if f == 'virtual_available':
356 c.update({ 'states': ('confirmed','waiting','assigned','done'), 'what': ('in', 'out') })
357 if f == 'incoming_qty':
358 c.update({ 'states': ('confirmed','waiting','assigned'), 'what': ('in',) })
359 if f == 'outgoing_qty':
360 c.update({ 'states': ('confirmed','waiting','assigned'), 'what': ('out',) })
361 stock = self.get_product_available(cr, uid, ids, context=c)
363 res[id][f] = stock.get(id, 0.0)
367 'reception_count': fields.function(_stock_move_count, string="Reception", type='integer', multi='pickings'),
368 'delivery_count': fields.function(_stock_move_count, string="Delivery", type='integer', multi='pickings'),
369 'qty_available': fields.function(_product_available, multi='qty_available',
370 type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
371 string='Quantity On Hand',
372 help="Current quantity of products.\n"
373 "In a context with a single Stock Location, this includes "
374 "goods stored at this Location, or any of its children.\n"
375 "In a context with a single Warehouse, this includes "
376 "goods stored in the Stock Location of this Warehouse, or any "
378 "In a context with a single Shop, this includes goods "
379 "stored in the Stock Location of the Warehouse of this Shop, "
380 "or any of its children.\n"
381 "Otherwise, this includes goods stored in any Stock Location "
382 "with 'internal' type."),
383 'virtual_available': fields.function(_product_available, multi='qty_available',
384 type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
385 string='Forecasted Quantity',
386 help="Forecast quantity (computed as Quantity On Hand "
387 "- Outgoing + Incoming)\n"
388 "In a context with a single Stock Location, this includes "
389 "goods stored in this location, or any of its children.\n"
390 "In a context with a single Warehouse, this includes "
391 "goods stored in the Stock Location of this Warehouse, or any "
393 "In a context with a single Shop, this includes goods "
394 "stored in the Stock Location of the Warehouse of this Shop, "
395 "or any of its children.\n"
396 "Otherwise, this includes goods stored in any Stock Location "
397 "with 'internal' type."),
398 'incoming_qty': fields.function(_product_available, multi='qty_available',
399 type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
401 help="Quantity of products that are planned to arrive.\n"
402 "In a context with a single Stock Location, this includes "
403 "goods arriving to this Location, or any of its children.\n"
404 "In a context with a single Warehouse, this includes "
405 "goods arriving to the Stock Location of this Warehouse, or "
406 "any of its children.\n"
407 "In a context with a single Shop, this includes goods "
408 "arriving to the Stock Location of the Warehouse of this "
409 "Shop, or any of its children.\n"
410 "Otherwise, this includes goods arriving to any Stock "
411 "Location with 'internal' type."),
412 'outgoing_qty': fields.function(_product_available, multi='qty_available',
413 type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
415 help="Quantity of products that are planned to leave.\n"
416 "In a context with a single Stock Location, this includes "
417 "goods leaving this Location, or any of its children.\n"
418 "In a context with a single Warehouse, this includes "
419 "goods leaving the Stock Location of this Warehouse, or "
420 "any of its children.\n"
421 "In a context with a single Shop, this includes goods "
422 "leaving the Stock Location of the Warehouse of this "
423 "Shop, or any of its children.\n"
424 "Otherwise, this includes goods leaving any Stock "
425 "Location with 'internal' type."),
426 '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"),
427 '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"),
428 '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"),
429 'location_id': fields.dummy(string='Location', relation='stock.location', type='many2one'),
430 'warehouse_id': fields.dummy(string='Warehouse', relation='stock.warehouse', type='many2one'),
431 'valuation':fields.selection([('manual_periodic', 'Periodical (manual)'),
432 ('real_time','Real Time (automated)'),], 'Inventory Valuation',
433 help="If real-time valuation is enabled for a product, the system will automatically write journal entries corresponding to stock moves." \
434 "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."
439 'valuation': 'manual_periodic',
442 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
443 res = super(product_product,self).fields_view_get(cr, uid, view_id, view_type, context, toolbar=toolbar, submenu=submenu)
446 if ('location' in context) and context['location']:
447 location_info = self.pool.get('stock.location').browse(cr, uid, context['location'])
448 fields=res.get('fields',{})
450 if location_info.usage == 'supplier':
451 if fields.get('virtual_available'):
452 res['fields']['virtual_available']['string'] = _('Future Receptions')
453 if fields.get('qty_available'):
454 res['fields']['qty_available']['string'] = _('Received Qty')
456 if location_info.usage == 'internal':
457 if fields.get('virtual_available'):
458 res['fields']['virtual_available']['string'] = _('Future Stock')
460 if location_info.usage == 'customer':
461 if fields.get('virtual_available'):
462 res['fields']['virtual_available']['string'] = _('Future Deliveries')
463 if fields.get('qty_available'):
464 res['fields']['qty_available']['string'] = _('Delivered Qty')
466 if location_info.usage == 'inventory':
467 if fields.get('virtual_available'):
468 res['fields']['virtual_available']['string'] = _('Future P&L')
469 if fields.get('qty_available'):
470 res['fields']['qty_available']['string'] = _('P&L Qty')
472 if location_info.usage == 'procurement':
473 if fields.get('virtual_available'):
474 res['fields']['virtual_available']['string'] = _('Future Qty')
475 if fields.get('qty_available'):
476 res['fields']['qty_available']['string'] = _('Unplanned Qty')
478 if location_info.usage == 'production':
479 if fields.get('virtual_available'):
480 res['fields']['virtual_available']['string'] = _('Future Productions')
481 if fields.get('qty_available'):
482 res['fields']['qty_available']['string'] = _('Produced Qty')
487 class product_template(osv.osv):
488 _name = 'product.template'
489 _inherit = 'product.template'
491 'property_stock_procurement': fields.property(
494 relation='stock.location',
495 string="Procurement Location",
497 domain=[('usage','like','procurement')],
498 help="This stock location will be used, instead of the default one, as the source location for stock moves generated by procurements."),
499 'property_stock_production': fields.property(
502 relation='stock.location',
503 string="Production Location",
505 domain=[('usage','like','production')],
506 help="This stock location will be used, instead of the default one, as the source location for stock moves generated by manufacturing orders."),
507 'property_stock_inventory': fields.property(
510 relation='stock.location',
511 string="Inventory Location",
513 domain=[('usage','like','inventory')],
514 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."),
515 'property_stock_account_input': fields.property('account.account',
516 type='many2one', relation='account.account',
517 string='Stock Input Account', view_load=True,
518 help="When doing real-time inventory valuation, counterpart journal items for all incoming stock moves will be posted in this account, unless "
519 "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."),
520 'property_stock_account_output': fields.property('account.account',
521 type='many2one', relation='account.account',
522 string='Stock Output Account', view_load=True,
523 help="When doing real-time inventory valuation, counterpart journal items for all outgoing stock moves will be posted in this account, unless "
524 "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."),
525 '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."),
526 'loc_rack': fields.char('Rack', size=16),
527 'loc_row': fields.char('Row', size=16),
528 'loc_case': fields.char('Case', size=16),
536 class product_category(osv.osv):
538 _inherit = 'product.category'
540 'property_stock_journal': fields.property('account.journal',
541 relation='account.journal', type='many2one',
542 string='Stock Journal', view_load=True,
543 help="When doing real-time inventory valuation, this is the Accounting Journal in which entries will be automatically posted when stock moves are processed."),
544 'property_stock_account_input_categ': fields.property('account.account',
545 type='many2one', relation='account.account',
546 string='Stock Input Account', view_load=True,
547 help="When doing real-time inventory valuation, counterpart journal items for all incoming stock moves will be posted in this account, unless "
548 "there is a specific valuation account set on the source location. This is the default value for all products in this category. It "
549 "can also directly be set on each product"),
550 'property_stock_account_output_categ': fields.property('account.account',
551 type='many2one', relation='account.account',
552 string='Stock Output Account', view_load=True,
553 help="When doing real-time inventory valuation, counterpart journal items for all outgoing stock moves will be posted in this account, unless "
554 "there is a specific valuation account set on the destination location. This is the default value for all products in this category. It "
555 "can also directly be set on each product"),
556 'property_stock_valuation_account_id': fields.property('account.account',
558 relation='account.account',
559 string="Stock Valuation Account",
561 help="When real-time inventory valuation is enabled on a product, this account will hold the current value of the products.",),
566 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: