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)
58 stock_input_acc = product_obj.property_stock_account_input and product_obj.property_stock_account_input.id or False
59 if not stock_input_acc:
60 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 stock_output_acc = product_obj.property_stock_account_output and product_obj.property_stock_account_output.id or False
63 if not stock_output_acc:
64 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 journal_id = product_obj.categ_id.property_stock_journal and product_obj.categ_id.property_stock_journal.id or False
67 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 'stock_account_input': stock_input_acc,
70 'stock_account_output': stock_output_acc,
71 'stock_journal': journal_id,
72 'property_stock_valuation_account_id': account_valuation
75 def do_change_standard_price(self, cr, uid, ids, datas, context=None):
76 """ Changes the Standard Price of Product and creates an account move accordingly.
77 @param datas : dict. contain default datas like new_price, stock_output_account, stock_input_account, stock_journal
78 @param context: A standard dictionary
82 location_obj = self.pool.get('stock.location')
83 move_obj = self.pool.get('account.move')
84 move_line_obj = self.pool.get('account.move.line')
88 new_price = datas.get('new_price', 0.0)
89 stock_output_acc = datas.get('stock_output_account', False)
90 stock_input_acc = datas.get('stock_input_account', False)
91 journal_id = datas.get('stock_journal', False)
92 product_obj=self.browse(cr, uid, ids, context=context)[0]
93 account_valuation = product_obj.categ_id.property_stock_valuation_account_id
94 account_valuation_id = account_valuation and account_valuation.id or False
95 if not account_valuation_id: raise osv.except_osv(_('Error!'), _('Specify valuation Account for Product Category: %s.') % (product_obj.categ_id.name))
97 loc_ids = location_obj.search(cr, uid,[('usage','=','internal')])
99 for location in location_obj.browse(cr, uid, loc_ids, context=context):
102 'location': location.id,
103 'compute_child': False
106 product = self.browse(cr, uid, rec_id, context=c)
107 qty = product.qty_available
108 diff = product.standard_price - new_price
109 if not diff: raise osv.except_osv(_('Error!'), _("No difference between standard price and new price!"))
111 company_id = location.company_id and location.company_id.id or False
112 if not company_id: raise osv.except_osv(_('Error!'), _('Please specify company in Location.'))
117 journal_id = product.categ_id.property_stock_journal and product.categ_id.property_stock_journal.id or False
119 raise osv.except_osv(_('Error!'),
120 _('Please define journal '\
121 'on the product category: "%s" (id: %d).') % \
122 (product.categ_id.name,
123 product.categ_id.id,))
124 move_id = move_obj.create(cr, uid, {
125 'journal_id': journal_id,
126 'company_id': company_id
129 move_ids.append(move_id)
133 if not stock_input_acc:
134 stock_input_acc = product.\
135 property_stock_account_input.id
136 if not stock_input_acc:
137 stock_input_acc = product.categ_id.\
138 property_stock_account_input_categ.id
139 if not stock_input_acc:
140 raise osv.except_osv(_('Error!'),
141 _('Please define stock input account ' \
142 'for this product: "%s" (id: %d).') % \
145 amount_diff = qty * diff
146 move_line_obj.create(cr, uid, {
147 'name': product.name,
148 'account_id': stock_input_acc,
149 'debit': amount_diff,
152 move_line_obj.create(cr, uid, {
153 'name': product.categ_id.name,
154 'account_id': account_valuation_id,
155 'credit': amount_diff,
159 if not stock_output_acc:
160 stock_output_acc = product.\
161 property_stock_account_output.id
162 if not stock_output_acc:
163 stock_output_acc = product.categ_id.\
164 property_stock_account_output_categ.id
165 if not stock_output_acc:
166 raise osv.except_osv(_('Error!'),
167 _('Please define stock output account ' \
168 'for this product: "%s" (id: %d).') % \
171 amount_diff = qty * -diff
172 move_line_obj.create(cr, uid, {
173 'name': product.name,
174 'account_id': stock_output_acc,
175 'credit': amount_diff,
178 move_line_obj.create(cr, uid, {
179 'name': product.categ_id.name,
180 'account_id': account_valuation_id,
181 'debit': amount_diff,
185 self.write(cr, uid, rec_id, {'standard_price': new_price})
189 def view_header_get(self, cr, user, view_id, view_type, context=None):
192 res = super(product_product, self).view_header_get(cr, user, view_id, view_type, context)
194 if (context.get('active_id', False)) and (context.get('active_model') == 'stock.location'):
195 return _('Products: ')+self.pool.get('stock.location').browse(cr, user, context['active_id'], context).name
198 def get_product_available(self, cr, uid, ids, context=None):
199 """ Finds whether product is available or not in particular warehouse.
200 @return: Dictionary of values
205 location_obj = self.pool.get('stock.location')
206 warehouse_obj = self.pool.get('stock.warehouse')
207 shop_obj = self.pool.get('sale.shop')
209 states = context.get('states',[])
210 what = context.get('what',())
212 ids = self.search(cr, uid, [])
213 res = {}.fromkeys(ids, 0.0)
217 if context.get('shop', False):
218 warehouse_id = shop_obj.read(cr, uid, int(context['shop']), ['warehouse_id'])['warehouse_id'][0]
220 context['warehouse'] = warehouse_id
222 if context.get('warehouse', False):
223 lot_id = warehouse_obj.read(cr, uid, int(context['warehouse']), ['lot_stock_id'])['lot_stock_id'][0]
225 context['location'] = lot_id
227 if context.get('location', False):
228 if type(context['location']) == type(1):
229 location_ids = [context['location']]
230 elif type(context['location']) in (type(''), type(u'')):
231 location_ids = location_obj.search(cr, uid, [('name','ilike',context['location'])], context=context)
233 location_ids = context['location']
236 wids = warehouse_obj.search(cr, uid, [], context=context)
239 for w in warehouse_obj.browse(cr, uid, wids, context=context):
240 location_ids.append(w.lot_stock_id.id)
242 # build the list of ids of children of the location given by id
243 if context.get('compute_child',True):
244 child_location_ids = location_obj.search(cr, uid, [('location_id', 'child_of', location_ids)])
245 location_ids = child_location_ids or location_ids
247 # this will be a dictionary of the product UoM by product id
250 for product in self.read(cr, uid, ids, ['uom_id'], context=context):
251 product2uom[product['id']] = product['uom_id'][0]
252 uom_ids.append(product['uom_id'][0])
253 # this will be a dictionary of the UoM resources we need for conversion purposes, by UoM id
255 for uom in self.pool.get('product.uom').browse(cr, uid, uom_ids, context=context):
261 from_date = context.get('from_date',False)
262 to_date = context.get('to_date',False)
265 where = [tuple(location_ids),tuple(location_ids),tuple(ids),tuple(states)]
266 if from_date and to_date:
267 date_str = "date>=%s and date<=%s"
268 where.append(tuple([from_date]))
269 where.append(tuple([to_date]))
271 date_str = "date>=%s"
272 date_values = [from_date]
274 date_str = "date<=%s"
275 date_values = [to_date]
277 where.append(tuple(date_values))
279 prodlot_id = context.get('prodlot_id', False)
282 prodlot_clause = ' and prodlot_id = %s '
283 where += [prodlot_id]
285 # TODO: perhaps merge in one query.
287 # all moves from a location out of the set to a location in the set
289 'select sum(product_qty), product_id, product_uom '\
291 'where location_id NOT IN %s '\
292 'and location_dest_id IN %s '\
293 'and product_id IN %s '\
294 'and state IN %s ' + (date_str and 'and '+date_str+' ' or '') +' '\
296 'group by product_id,product_uom',tuple(where))
297 results = cr.fetchall()
299 # all moves from a location in the set to a location out of the set
301 'select sum(product_qty), product_id, product_uom '\
303 'where location_id IN %s '\
304 'and location_dest_id NOT IN %s '\
305 'and product_id IN %s '\
306 'and state in %s ' + (date_str and 'and '+date_str+' ' or '') + ' '\
308 'group by product_id,product_uom',tuple(where))
309 results2 = cr.fetchall()
311 # Get the missing UoM resources
312 uom_obj = self.pool.get('product.uom')
313 uoms = map(lambda x: x[2], results) + map(lambda x: x[2], results2)
314 if context.get('uom', False):
315 uoms += [context['uom']]
316 uoms = filter(lambda x: x not in uoms_o.keys(), uoms)
318 uoms = uom_obj.browse(cr, uid, list(set(uoms)), context=context)
322 #TOCHECK: before change uom of product, stock move line are in old uom.
323 context.update({'raise-exception': False})
324 # Count the incoming quantities
325 for amount, prod_id, prod_uom in results:
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
329 # Count the outgoing quantities
330 for amount, prod_id, prod_uom in results2:
331 amount = uom_obj._compute_qty_obj(cr, uid, uoms_o[prod_uom], amount,
332 uoms_o[context.get('uom', False) or product2uom[prod_id]], context=context)
333 res[prod_id] -= amount
336 def _product_available(self, cr, uid, ids, field_names=None, arg=False, context=None):
337 """ Finds the incoming and outgoing quantity of product.
338 @return: Dictionary of values
346 res[id] = {}.fromkeys(field_names, 0.0)
347 for f in field_names:
349 if f == 'qty_available':
350 c.update({ 'states': ('done',), 'what': ('in', 'out') })
351 if f == 'virtual_available':
352 c.update({ 'states': ('confirmed','waiting','assigned','done'), 'what': ('in', 'out') })
353 if f == 'incoming_qty':
354 c.update({ 'states': ('confirmed','waiting','assigned'), 'what': ('in',) })
355 if f == 'outgoing_qty':
356 c.update({ 'states': ('confirmed','waiting','assigned'), 'what': ('out',) })
357 stock = self.get_product_available(cr, uid, ids, context=c)
359 res[id][f] = stock.get(id, 0.0)
363 'reception_count': fields.function(_stock_move_count, string="Reception", type='integer', multi='pickings'),
364 'delivery_count': fields.function(_stock_move_count, string="Delivery", type='integer', multi='pickings'),
365 'qty_available': fields.function(_product_available, multi='qty_available',
366 type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
367 string='Quantity On Hand',
368 help="Current quantity of products.\n"
369 "In a context with a single Stock Location, this includes "
370 "goods stored at this Location, or any of its children.\n"
371 "In a context with a single Warehouse, this includes "
372 "goods stored in the Stock Location of this Warehouse, or any "
374 "In a context with a single Shop, this includes goods "
375 "stored in the Stock Location of the Warehouse of this Shop, "
376 "or any of its children.\n"
377 "Otherwise, this includes goods stored in any Stock Location "
378 "with 'internal' type."),
379 'virtual_available': fields.function(_product_available, multi='qty_available',
380 type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
381 string='Forecasted Quantity',
382 help="Forecast quantity (computed as Quantity On Hand "
383 "- Outgoing + Incoming)\n"
384 "In a context with a single Stock Location, this includes "
385 "goods stored in this location, or any of its children.\n"
386 "In a context with a single Warehouse, this includes "
387 "goods stored in the Stock Location of this Warehouse, or any "
389 "In a context with a single Shop, this includes goods "
390 "stored in the Stock Location of the Warehouse of this Shop, "
391 "or any of its children.\n"
392 "Otherwise, this includes goods stored in any Stock Location "
393 "with 'internal' type."),
394 'incoming_qty': fields.function(_product_available, multi='qty_available',
395 type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
397 help="Quantity of products that are planned to arrive.\n"
398 "In a context with a single Stock Location, this includes "
399 "goods arriving to this Location, or any of its children.\n"
400 "In a context with a single Warehouse, this includes "
401 "goods arriving to the Stock Location of this Warehouse, or "
402 "any of its children.\n"
403 "In a context with a single Shop, this includes goods "
404 "arriving to the Stock Location of the Warehouse of this "
405 "Shop, or any of its children.\n"
406 "Otherwise, this includes goods arriving to any Stock "
407 "Location with 'internal' type."),
408 'outgoing_qty': fields.function(_product_available, multi='qty_available',
409 type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
411 help="Quantity of products that are planned to leave.\n"
412 "In a context with a single Stock Location, this includes "
413 "goods leaving this Location, or any of its children.\n"
414 "In a context with a single Warehouse, this includes "
415 "goods leaving the Stock Location of this Warehouse, or "
416 "any of its children.\n"
417 "In a context with a single Shop, this includes goods "
418 "leaving the Stock Location of the Warehouse of this "
419 "Shop, or any of its children.\n"
420 "Otherwise, this includes goods leaving any Stock "
421 "Location with 'internal' type."),
422 '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"),
423 '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"),
424 '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"),
425 'location_id': fields.dummy(string='Location', relation='stock.location', type='many2one'),
426 'warehouse_id': fields.dummy(string='Warehouse', relation='stock.warehouse', type='many2one'),
427 'valuation':fields.selection([('manual_periodic', 'Periodical (manual)'),
428 ('real_time','Real Time (automated)'),], 'Inventory Valuation',
429 help="If real-time valuation is enabled for a product, the system will automatically write journal entries corresponding to stock moves." \
430 "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."
435 'valuation': 'manual_periodic',
438 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
439 res = super(product_product,self).fields_view_get(cr, uid, view_id, view_type, context, toolbar=toolbar, submenu=submenu)
442 if ('location' in context) and context['location']:
443 location_info = self.pool.get('stock.location').browse(cr, uid, context['location'])
444 fields=res.get('fields',{})
446 if location_info.usage == 'supplier':
447 if fields.get('virtual_available'):
448 res['fields']['virtual_available']['string'] = _('Future Receptions')
449 if fields.get('qty_available'):
450 res['fields']['qty_available']['string'] = _('Received Qty')
452 if location_info.usage == 'internal':
453 if fields.get('virtual_available'):
454 res['fields']['virtual_available']['string'] = _('Future Stock')
456 if location_info.usage == 'customer':
457 if fields.get('virtual_available'):
458 res['fields']['virtual_available']['string'] = _('Future Deliveries')
459 if fields.get('qty_available'):
460 res['fields']['qty_available']['string'] = _('Delivered Qty')
462 if location_info.usage == 'inventory':
463 if fields.get('virtual_available'):
464 res['fields']['virtual_available']['string'] = _('Future P&L')
465 if fields.get('qty_available'):
466 res['fields']['qty_available']['string'] = _('P&L Qty')
468 if location_info.usage == 'procurement':
469 if fields.get('virtual_available'):
470 res['fields']['virtual_available']['string'] = _('Future Qty')
471 if fields.get('qty_available'):
472 res['fields']['qty_available']['string'] = _('Unplanned Qty')
474 if location_info.usage == 'production':
475 if fields.get('virtual_available'):
476 res['fields']['virtual_available']['string'] = _('Future Productions')
477 if fields.get('qty_available'):
478 res['fields']['qty_available']['string'] = _('Produced Qty')
483 class product_template(osv.osv):
484 _name = 'product.template'
485 _inherit = 'product.template'
487 'property_stock_procurement': fields.property(
490 relation='stock.location',
491 string="Procurement Location",
493 domain=[('usage','like','procurement')],
494 help="This stock location will be used, instead of the default one, as the source location for stock moves generated by procurements."),
495 'property_stock_production': fields.property(
498 relation='stock.location',
499 string="Production Location",
501 domain=[('usage','like','production')],
502 help="This stock location will be used, instead of the default one, as the source location for stock moves generated by manufacturing orders."),
503 'property_stock_inventory': fields.property(
506 relation='stock.location',
507 string="Inventory Location",
509 domain=[('usage','like','inventory')],
510 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."),
511 'property_stock_account_input': fields.property('account.account',
512 type='many2one', relation='account.account',
513 string='Stock Input Account', view_load=True,
514 help="When doing real-time inventory valuation, counterpart journal items for all incoming stock moves will be posted in this account, unless "
515 "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."),
516 'property_stock_account_output': fields.property('account.account',
517 type='many2one', relation='account.account',
518 string='Stock Output Account', view_load=True,
519 help="When doing real-time inventory valuation, counterpart journal items for all outgoing stock moves will be posted in this account, unless "
520 "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."),
521 '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."),
522 'loc_rack': fields.char('Rack', size=16),
523 'loc_row': fields.char('Row', size=16),
524 'loc_case': fields.char('Case', size=16),
532 class product_category(osv.osv):
534 _inherit = 'product.category'
536 'property_stock_journal': fields.property('account.journal',
537 relation='account.journal', type='many2one',
538 string='Stock Journal', view_load=True,
539 help="When doing real-time inventory valuation, this is the Accounting Journal in which entries will be automatically posted when stock moves are processed."),
540 'property_stock_account_input_categ': fields.property('account.account',
541 type='many2one', relation='account.account',
542 string='Stock Input Account', view_load=True,
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. This is the default value for all products in this category. It "
545 "can also directly be set on each product"),
546 'property_stock_account_output_categ': fields.property('account.account',
547 type='many2one', relation='account.account',
548 string='Stock Output Account', view_load=True,
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. This is the default value for all products in this category. It "
551 "can also directly be set on each product"),
552 'property_stock_valuation_account_id': fields.property('account.account',
554 relation='account.account',
555 string="Stock Valuation Account",
557 help="When real-time inventory valuation is enabled on a product, this account will hold the current value of the products.",),
562 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: