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)
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))
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!'), _("No 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!'), _('Please specify company 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 _('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
130 move_ids.append(move_id)
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).') % \
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.\
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).') % \
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_locations_from_context(self, cr, uid, ids, context=None):
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
207 location_obj = self.pool.get('stock.location')
208 warehouse_obj = self.pool.get('stock.warehouse')
209 shop_obj = self.pool.get('sale.shop')
210 if context.get('shop', False):
211 warehouse_id = shop_obj.read(cr, uid, int(context['shop']), ['warehouse_id'])['warehouse_id'][0]
213 context['warehouse'] = warehouse_id
215 if context.get('warehouse', False):
216 lot_id = warehouse_obj.read(cr, uid, int(context['warehouse']), ['lot_stock_id'])['lot_stock_id'][0]
218 context['location'] = lot_id
220 if context.get('location', False):
221 if type(context['location']) == type(1):
222 location_ids = [context['location']]
223 elif type(context['location']) in (type(''), type(u'')):
224 if context.get('force_company', False):
225 location_ids = location_obj.search(cr, uid, [('name','ilike',context['location']), ('company_id', '=', context['force_company'])], context=context)
227 location_ids = location_obj.search(cr, uid, [('name','ilike',context['location'])], context=context)
229 location_ids = context['location']
232 wids = warehouse_obj.search(cr, uid, [], context=context)
235 for w in warehouse_obj.browse(cr, uid, wids, context=context):
236 if not context.get('force_company', False) or w.lot_stock_id.company_id.id == context['force_company']:
237 location_ids.append(w.lot_stock_id.id)
239 # build the list of ids of children of the location given by id
240 if context.get('compute_child',True):
241 if context.get('force_company', False):
242 child_location_ids = location_obj.search(cr, uid, [('location_id', 'child_of', location_ids), ('company_id', '=', context['force_company'])])
244 child_location_ids = location_obj.search(cr, uid, [('location_id', 'child_of', location_ids)])
245 location_ids = child_location_ids or location_ids
249 def _get_date_query(self, cr, uid, ids, context):
251 Parses the context and returns the dates query string needed to be processed in _get_product_available
252 It searches for a from_date and to_date
254 from_date = context.get('from_date',False)
255 to_date = context.get('to_date',False)
259 if from_date and to_date:
260 date_str = "date>=%s and date<=%s"
261 whereadd.append(tuple([from_date]))
262 whereadd.append(tuple([to_date]))
264 date_str = "date>=%s"
265 whereadd.append(tuple([from_date]))
267 date_str = "date<=%s"
268 whereadd.append(tuple([to_date]))
269 return (whereadd, date_str)
274 def get_product_available(self, cr, uid, ids, context=None):
275 """ Finds the quantity available of product(s) depending on parameters in the context
276 for what, states, locations (company, warehouse, ), date, lot,
277 states: state of the move
278 what: in (dest in locations) or out (source in locations) moves
280 shop: warehouse of the shop
281 warehouse: stock location of the warehouse
282 location: name (ilike) or id of the location
283 force_company: if not warehouse or shop given: will only take from this company
284 compute_child (True if not specified): will also include child locations of locations above
285 (when force_company only from that company)
287 from_date and to_date: dates from or to for the date of the stock move to include (=scheduled of effective date)
288 prodlot: lot of the move
290 @return: Dictionary of values for every product id
295 states = context.get('states',[])
296 what = context.get('what',())
298 ids = self.search(cr, uid, [])
299 res = {}.fromkeys(ids, 0.0)
302 #set_context: refactor code here
303 location_ids = self._get_locations_from_context(cr, uid, ids, context=context)
304 if not location_ids: #in case of no locations, query will be empty anyways
307 # this will be a dictionary of the product UoM by product id
310 for product in self.read(cr, uid, ids, ['uom_id'], context=context):
311 product2uom[product['id']] = product['uom_id'][0]
312 uom_ids.append(product['uom_id'][0])
313 # this will be a dictionary of the UoM resources we need for conversion purposes, by UoM id
315 for uom in self.pool.get('product.uom').browse(cr, uid, uom_ids, context=context):
321 where = [tuple(location_ids),tuple(location_ids),tuple(ids),tuple(states)]
323 where_add, date_str = self._get_date_query(cr, uid, ids, context=context)
328 prodlot_id = context.get('prodlot_id', False)
331 prodlot_clause = ' and prodlot_id = %s '
332 where += [prodlot_id]
334 # TODO: perhaps merge in one query.
336 # all moves from a location out of the set to a location in the set
338 'select sum(product_qty), product_id, product_uom '\
340 'where location_id NOT IN %s '\
341 'and location_dest_id IN %s '\
342 'and product_id IN %s '\
343 'and state IN %s ' + (date_str and 'and '+date_str+' ' or '') +' '\
345 'group by product_id,product_uom',tuple(where))
346 results = cr.fetchall()
348 # all moves from a location in the set to a location out of the set
350 'select sum(product_qty), product_id, product_uom '\
352 'where location_id IN %s '\
353 'and location_dest_id NOT IN %s '\
354 'and product_id IN %s '\
355 'and state in %s ' + (date_str and 'and '+date_str+' ' or '') + ' '\
357 'group by product_id,product_uom',tuple(where))
358 results2 = cr.fetchall()
360 # Get the missing UoM resources
361 uom_obj = self.pool.get('product.uom')
362 uoms = map(lambda x: x[2], results) + map(lambda x: x[2], results2)
363 if context.get('uom', False):
364 uoms += [context['uom']]
365 uoms = filter(lambda x: x not in uoms_o.keys(), uoms)
367 uoms = uom_obj.browse(cr, uid, list(set(uoms)), context=context)
371 #TOCHECK: before change uom of product, stock move line are in old uom.
372 context.update({'raise-exception': False})
373 # Count the incoming quantities
374 for amount, prod_id, prod_uom in results:
375 amount = uom_obj._compute_qty_obj(cr, uid, uoms_o[prod_uom], amount,
376 uoms_o[context.get('uom', False) or product2uom[prod_id]], context=context)
377 res[prod_id] += amount
378 # Count the outgoing quantities
379 for amount, prod_id, prod_uom in results2:
380 amount = uom_obj._compute_qty_obj(cr, uid, uoms_o[prod_uom], amount,
381 uoms_o[context.get('uom', False) or product2uom[prod_id]], context=context)
382 res[prod_id] -= amount
385 def _product_available(self, cr, uid, ids, field_names=None, arg=False, context=None):
386 """ Finds the incoming and outgoing quantity of product.
387 @return: Dictionary of values
395 res[id] = {}.fromkeys(field_names, 0.0)
396 for f in field_names:
398 if f == 'qty_available':
399 c.update({ 'states': ('done',), 'what': ('in', 'out') })
400 if f == 'virtual_available':
401 c.update({ 'states': ('confirmed','waiting','assigned','done'), 'what': ('in', 'out') })
402 if f == 'incoming_qty':
403 c.update({ 'states': ('confirmed','waiting','assigned'), 'what': ('in',) })
404 if f == 'outgoing_qty':
405 c.update({ 'states': ('confirmed','waiting','assigned'), 'what': ('out',) })
406 stock = self.get_product_available(cr, uid, ids, context=c)
408 res[id][f] = stock.get(id, 0.0)
412 'reception_count': fields.function(_stock_move_count, string="Reception", type='integer', multi='pickings'),
413 'delivery_count': fields.function(_stock_move_count, string="Delivery", type='integer', multi='pickings'),
414 'qty_available': fields.function(_product_available, multi='qty_available',
415 type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
416 string='Quantity On Hand',
417 help="Current quantity of products.\n"
418 "In a context with a single Stock Location, this includes "
419 "goods stored at this Location, or any of its children.\n"
420 "In a context with a single Warehouse, this includes "
421 "goods stored in the Stock Location of this Warehouse, or any "
423 "stored in the Stock Location of the Warehouse of this Shop, "
424 "or any of its children.\n"
425 "Otherwise, this includes goods stored in any Stock Location "
426 "with 'internal' type."),
427 'virtual_available': fields.function(_product_available, multi='qty_available',
428 type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
429 string='Forecasted Quantity',
430 help="Forecast quantity (computed as Quantity On Hand "
431 "- Outgoing + Incoming)\n"
432 "In a context with a single Stock Location, this includes "
433 "goods stored in this location, or any of its children.\n"
434 "In a context with a single Warehouse, this includes "
435 "goods stored in the Stock Location of this Warehouse, or any "
437 "stored in the Stock Location of the Warehouse of this Shop, "
438 "or any of its children.\n"
439 "Otherwise, this includes goods stored in any Stock Location "
440 "with 'internal' type."),
441 'incoming_qty': fields.function(_product_available, multi='qty_available',
442 type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
444 help="Quantity of products that are planned to arrive.\n"
445 "In a context with a single Stock Location, this includes "
446 "goods arriving to this Location, or any of its children.\n"
447 "In a context with a single Warehouse, this includes "
448 "goods arriving to the Stock Location of this Warehouse, or "
449 "any of its children.\n"
450 "In a context with a single Shop, this includes goods "
451 "arriving to the Stock Location of the Warehouse of this "
452 "Shop, or any of its children.\n"
453 "Otherwise, this includes goods arriving to any Stock "
454 "Location with 'internal' type."),
455 'outgoing_qty': fields.function(_product_available, multi='qty_available',
456 type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
458 help="Quantity of products that are planned to leave.\n"
459 "In a context with a single Stock Location, this includes "
460 "goods leaving this Location, or any of its children.\n"
461 "In a context with a single Warehouse, this includes "
462 "goods leaving the Stock Location of this Warehouse, or "
463 "any of its children.\n"
464 "In a context with a single Shop, this includes goods "
465 "leaving the Stock Location of the Warehouse of this "
466 "Shop, or any of its children.\n"
467 "Otherwise, this includes goods leaving any Stock "
468 "Location with 'internal' type."),
469 '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"),
470 '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"),
471 '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"),
472 'location_id': fields.dummy(string='Location', relation='stock.location', type='many2one'),
473 'warehouse_id': fields.dummy(string='Warehouse', relation='stock.warehouse', type='many2one'),
474 'valuation':fields.property(type='selection', selection=[('manual_periodic', 'Periodical (manual)'),
475 ('real_time','Real Time (automated)'),], string = 'Inventory Valuation',
476 help="If real-time valuation is enabled for a product, the system will automatically write journal entries corresponding to stock moves." \
477 "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."
482 'valuation': 'manual_periodic',
485 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
486 res = super(product_product,self).fields_view_get(cr, uid, view_id, view_type, context, toolbar=toolbar, submenu=submenu)
489 if ('location' in context) and context['location']:
490 location_info = self.pool.get('stock.location').browse(cr, uid, context['location'])
491 fields=res.get('fields',{})
493 if location_info.usage == 'supplier':
494 if fields.get('virtual_available'):
495 res['fields']['virtual_available']['string'] = _('Future Receptions')
496 if fields.get('qty_available'):
497 res['fields']['qty_available']['string'] = _('Received Qty')
499 if location_info.usage == 'internal':
500 if fields.get('virtual_available'):
501 res['fields']['virtual_available']['string'] = _('Future Stock')
503 if location_info.usage == 'customer':
504 if fields.get('virtual_available'):
505 res['fields']['virtual_available']['string'] = _('Future Deliveries')
506 if fields.get('qty_available'):
507 res['fields']['qty_available']['string'] = _('Delivered Qty')
509 if location_info.usage == 'inventory':
510 if fields.get('virtual_available'):
511 res['fields']['virtual_available']['string'] = _('Future P&L')
512 if fields.get('qty_available'):
513 res['fields']['qty_available']['string'] = _('P&L Qty')
515 if location_info.usage == 'procurement':
516 if fields.get('virtual_available'):
517 res['fields']['virtual_available']['string'] = _('Future Qty')
518 if fields.get('qty_available'):
519 res['fields']['qty_available']['string'] = _('Unplanned Qty')
521 if location_info.usage == 'production':
522 if fields.get('virtual_available'):
523 res['fields']['virtual_available']['string'] = _('Future Productions')
524 if fields.get('qty_available'):
525 res['fields']['qty_available']['string'] = _('Produced Qty')
529 class product_template(osv.osv):
530 _name = 'product.template'
531 _inherit = 'product.template'
533 'property_stock_procurement': fields.property(
535 relation='stock.location',
536 string="Procurement Location",
537 domain=[('usage','like','procurement')],
538 help="This stock location will be used, instead of the default one, as the source location for stock moves generated by procurements."),
539 'property_stock_production': fields.property(
541 relation='stock.location',
542 string="Production Location",
543 domain=[('usage','like','production')],
544 help="This stock location will be used, instead of the default one, as the source location for stock moves generated by manufacturing orders."),
545 'property_stock_inventory': fields.property(
547 relation='stock.location',
548 string="Inventory Location",
549 domain=[('usage','like','inventory')],
550 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."),
551 'property_stock_account_input': fields.property(
553 relation='account.account',
554 string='Stock Input Account',
555 help="When doing real-time inventory valuation, counterpart journal items for all incoming stock moves will be posted in this account, unless "
556 "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."),
557 'property_stock_account_output': fields.property(
559 relation='account.account',
560 string='Stock Output Account',
561 help="When doing real-time inventory valuation, counterpart journal items for all outgoing stock moves will be posted in this account, unless "
562 "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."),
563 '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."),
564 'loc_rack': fields.char('Rack', size=16),
565 'loc_row': fields.char('Row', size=16),
566 'loc_case': fields.char('Case', size=16),
573 class product_category(osv.osv):
575 _inherit = 'product.category'
577 'removal_strategy': fields.selection([('fifo', 'FIFO'), ('lifo', 'LIFO'), ('nearest', 'Nearest Location')], "Standard Removal Strategy"),
578 'property_stock_journal': fields.property(
579 relation='account.journal',
581 string='Stock Journal',
582 help="When doing real-time inventory valuation, this is the Accounting Journal in which entries will be automatically posted when stock moves are processed."),
583 'property_stock_account_input_categ': fields.property(
585 relation='account.account',
586 string='Stock Input Account',
587 help="When doing real-time inventory valuation, counterpart journal items for all incoming stock moves will be posted in this account, unless "
588 "there is a specific valuation account set on the source location. This is the default value for all products in this category. It "
589 "can also directly be set on each product"),
590 'property_stock_account_output_categ': fields.property(
592 relation='account.account',
593 string='Stock Output Account',
594 help="When doing real-time inventory valuation, counterpart journal items for all outgoing stock moves will be posted in this account, unless "
595 "there is a specific valuation account set on the destination location. This is the default value for all products in this category. It "
596 "can also directly be set on each product"),
597 'property_stock_valuation_account_id': fields.property(
599 relation='account.account',
600 string="Stock Valuation Account",
601 help="When real-time inventory valuation is enabled on a product, this account will hold the current value of the products.",),
605 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: