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 = 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,
185 self.write(cr, uid, ids, {'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')
208 states = context.get('states',[])
209 what = context.get('what',())
211 ids = self.search(cr, uid, [])
212 res = {}.fromkeys(ids, 0.0)
216 if context.get('warehouse', False):
217 lot_id = warehouse_obj.read(cr, uid, int(context['warehouse']), ['lot_stock_id'])['lot_stock_id'][0]
219 context['location'] = lot_id
221 if context.get('location', False):
222 if type(context['location']) == type(1):
223 location_ids = [context['location']]
224 elif type(context['location']) in (type(''), type(u'')):
225 location_ids = location_obj.search(cr, uid, [('name','ilike',context['location'])], context=context)
227 location_ids = context['location']
230 wids = warehouse_obj.search(cr, uid, [], context=context)
233 for w in warehouse_obj.browse(cr, uid, wids, context=context):
234 location_ids.append(w.lot_stock_id.id)
236 # build the list of ids of children of the location given by id
237 if context.get('compute_child',True):
238 child_location_ids = location_obj.search(cr, uid, [('location_id', 'child_of', location_ids)])
239 location_ids = child_location_ids or location_ids
241 # this will be a dictionary of the product UoM by product id
244 for product in self.read(cr, uid, ids, ['uom_id'], context=context):
245 product2uom[product['id']] = product['uom_id'][0]
246 uom_ids.append(product['uom_id'][0])
247 # this will be a dictionary of the UoM resources we need for conversion purposes, by UoM id
249 for uom in self.pool.get('product.uom').browse(cr, uid, uom_ids, context=context):
255 from_date = context.get('from_date',False)
256 to_date = context.get('to_date',False)
259 where = [tuple(location_ids),tuple(location_ids),tuple(ids),tuple(states)]
260 if from_date and to_date:
261 date_str = "date>=%s and date<=%s"
262 where.append(tuple([from_date]))
263 where.append(tuple([to_date]))
265 date_str = "date>=%s"
266 date_values = [from_date]
268 date_str = "date<=%s"
269 date_values = [to_date]
271 where.append(tuple(date_values))
273 prodlot_id = context.get('prodlot_id', False)
276 prodlot_clause = ' and prodlot_id = %s '
277 where += [prodlot_id]
279 # TODO: perhaps merge in one query.
281 # all moves from a location out of the set to a location in the set
283 'select sum(product_qty), product_id, product_uom '\
285 'where location_id NOT IN %s '\
286 'and location_dest_id IN %s '\
287 'and product_id IN %s '\
288 'and state IN %s ' + (date_str and 'and '+date_str+' ' or '') +' '\
290 'group by product_id,product_uom',tuple(where))
291 results = cr.fetchall()
293 # all moves from a location in the set to a location out of the set
295 'select sum(product_qty), product_id, product_uom '\
297 'where location_id IN %s '\
298 'and location_dest_id NOT IN %s '\
299 'and product_id IN %s '\
300 'and state in %s ' + (date_str and 'and '+date_str+' ' or '') + ' '\
302 'group by product_id,product_uom',tuple(where))
303 results2 = cr.fetchall()
305 # Get the missing UoM resources
306 uom_obj = self.pool.get('product.uom')
307 uoms = map(lambda x: x[2], results) + map(lambda x: x[2], results2)
308 if context.get('uom', False):
309 uoms += [context['uom']]
310 uoms = filter(lambda x: x not in uoms_o.keys(), uoms)
312 uoms = uom_obj.browse(cr, uid, list(set(uoms)), context=context)
316 #TOCHECK: before change uom of product, stock move line are in old uom.
317 context.update({'raise-exception': False})
318 # Count the incoming quantities
319 for amount, prod_id, prod_uom in results:
320 amount = uom_obj._compute_qty_obj(cr, uid, uoms_o[prod_uom], amount,
321 uoms_o[context.get('uom', False) or product2uom[prod_id]], context=context)
322 res[prod_id] += amount
323 # Count the outgoing quantities
324 for amount, prod_id, prod_uom in results2:
325 amount = uom_obj._compute_qty_obj(cr, uid, uoms_o[prod_uom], amount,
326 uoms_o[context.get('uom', False) or product2uom[prod_id]], context=context)
327 res[prod_id] -= amount
330 def _product_available(self, cr, uid, ids, field_names=None, arg=False, context=None):
331 """ Finds the incoming and outgoing quantity of product.
332 @return: Dictionary of values
340 res[id] = {}.fromkeys(field_names, 0.0)
341 for f in field_names:
343 if f == 'qty_available':
344 c.update({ 'states': ('done',), 'what': ('in', 'out') })
345 if f == 'virtual_available':
346 c.update({ 'states': ('confirmed','waiting','assigned','done'), 'what': ('in', 'out') })
347 if f == 'incoming_qty':
348 c.update({ 'states': ('confirmed','waiting','assigned'), 'what': ('in',) })
349 if f == 'outgoing_qty':
350 c.update({ 'states': ('confirmed','waiting','assigned'), 'what': ('out',) })
351 stock = self.get_product_available(cr, uid, ids, context=c)
353 res[id][f] = stock.get(id, 0.0)
357 'reception_count': fields.function(_stock_move_count, string="Reception", type='integer', multi='pickings'),
358 'delivery_count': fields.function(_stock_move_count, string="Delivery", type='integer', multi='pickings'),
359 'qty_available': fields.function(_product_available, multi='qty_available',
360 type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
361 string='Quantity On Hand',
362 help="Current quantity of products.\n"
363 "In a context with a single Stock Location, this includes "
364 "goods stored at this Location, or any of its children.\n"
365 "In a context with a single Warehouse, this includes "
366 "goods stored in the Stock Location of this Warehouse, or any "
368 "stored in the Stock Location of the Warehouse of this Shop, "
369 "or any of its children.\n"
370 "Otherwise, this includes goods stored in any Stock Location "
371 "with 'internal' type."),
372 'virtual_available': fields.function(_product_available, multi='qty_available',
373 type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
374 string='Forecasted Quantity',
375 help="Forecast quantity (computed as Quantity On Hand "
376 "- Outgoing + Incoming)\n"
377 "In a context with a single Stock Location, this includes "
378 "goods stored in this location, or any of its children.\n"
379 "In a context with a single Warehouse, this includes "
380 "goods stored in the Stock Location of this Warehouse, or any "
382 "Otherwise, this includes goods stored in any Stock Location "
383 "with 'internal' type."),
384 'incoming_qty': fields.function(_product_available, multi='qty_available',
385 type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
387 help="Quantity of products that are planned to arrive.\n"
388 "In a context with a single Stock Location, this includes "
389 "goods arriving to this Location, or any of its children.\n"
390 "In a context with a single Warehouse, this includes "
391 "goods arriving to the Stock Location of this Warehouse, or "
392 "any of its children.\n"
393 "Otherwise, this includes goods arriving to any Stock "
394 "Location with 'internal' type."),
395 'outgoing_qty': fields.function(_product_available, multi='qty_available',
396 type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
398 help="Quantity of products that are planned to leave.\n"
399 "In a context with a single Stock Location, this includes "
400 "goods leaving this Location, or any of its children.\n"
401 "In a context with a single Warehouse, this includes "
402 "goods leaving the Stock Location of this Warehouse, or "
403 "any of its children.\n"
404 "Otherwise, this includes goods leaving any Stock "
405 "Location with 'internal' type."),
406 '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"),
407 '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"),
408 '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"),
409 'location_id': fields.dummy(string='Location', relation='stock.location', type='many2one'),
410 'warehouse_id': fields.dummy(string='Warehouse', relation='stock.warehouse', type='many2one'),
411 'valuation':fields.selection([('manual_periodic', 'Periodical (manual)'),
412 ('real_time','Real Time (automated)'),], 'Inventory Valuation',
413 help="If real-time valuation is enabled for a product, the system will automatically write journal entries corresponding to stock moves." \
414 "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."
419 'valuation': 'manual_periodic',
422 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
423 res = super(product_product,self).fields_view_get(cr, uid, view_id, view_type, context, toolbar=toolbar, submenu=submenu)
426 if ('location' in context) and context['location']:
427 location_info = self.pool.get('stock.location').browse(cr, uid, context['location'])
428 fields=res.get('fields',{})
430 if location_info.usage == 'supplier':
431 if fields.get('virtual_available'):
432 res['fields']['virtual_available']['string'] = _('Future Receptions')
433 if fields.get('qty_available'):
434 res['fields']['qty_available']['string'] = _('Received Qty')
436 if location_info.usage == 'internal':
437 if fields.get('virtual_available'):
438 res['fields']['virtual_available']['string'] = _('Future Stock')
440 if location_info.usage == 'customer':
441 if fields.get('virtual_available'):
442 res['fields']['virtual_available']['string'] = _('Future Deliveries')
443 if fields.get('qty_available'):
444 res['fields']['qty_available']['string'] = _('Delivered Qty')
446 if location_info.usage == 'inventory':
447 if fields.get('virtual_available'):
448 res['fields']['virtual_available']['string'] = _('Future P&L')
449 if fields.get('qty_available'):
450 res['fields']['qty_available']['string'] = _('P&L Qty')
452 if location_info.usage == 'procurement':
453 if fields.get('virtual_available'):
454 res['fields']['virtual_available']['string'] = _('Future Qty')
455 if fields.get('qty_available'):
456 res['fields']['qty_available']['string'] = _('Unplanned Qty')
458 if location_info.usage == 'production':
459 if fields.get('virtual_available'):
460 res['fields']['virtual_available']['string'] = _('Future Productions')
461 if fields.get('qty_available'):
462 res['fields']['qty_available']['string'] = _('Produced Qty')
466 class product_template(osv.osv):
467 _name = 'product.template'
468 _inherit = 'product.template'
470 'property_stock_procurement': fields.property(
472 relation='stock.location',
473 string="Procurement Location",
474 domain=[('usage','like','procurement')],
475 help="This stock location will be used, instead of the default one, as the source location for stock moves generated by procurements."),
476 'property_stock_production': fields.property(
478 relation='stock.location',
479 string="Production Location",
480 domain=[('usage','like','production')],
481 help="This stock location will be used, instead of the default one, as the source location for stock moves generated by manufacturing orders."),
482 'property_stock_inventory': fields.property(
484 relation='stock.location',
485 string="Inventory Location",
486 domain=[('usage','like','inventory')],
487 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."),
488 'property_stock_account_input': fields.property(
490 relation='account.account',
491 string='Stock Input Account',
492 help="When doing real-time inventory valuation, counterpart journal items for all incoming stock moves will be posted in this account, unless "
493 "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."),
494 'property_stock_account_output': fields.property(
496 relation='account.account',
497 string='Stock Output Account',
498 help="When doing real-time inventory valuation, counterpart journal items for all outgoing stock moves will be posted in this account, unless "
499 "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."),
500 '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."),
501 'loc_rack': fields.char('Rack', size=16),
502 'loc_row': fields.char('Row', size=16),
503 'loc_case': fields.char('Case', size=16),
510 class product_category(osv.osv):
512 _inherit = 'product.category'
514 'property_stock_journal': fields.property(
515 relation='account.journal',
517 string='Stock Journal',
518 help="When doing real-time inventory valuation, this is the Accounting Journal in which entries will be automatically posted when stock moves are processed."),
519 'property_stock_account_input_categ': fields.property(
521 relation='account.account',
522 string='Stock Input Account',
523 help="When doing real-time inventory valuation, counterpart journal items for all incoming stock moves will be posted in this account, unless "
524 "there is a specific valuation account set on the source location. This is the default value for all products in this category. It "
525 "can also directly be set on each product"),
526 'property_stock_account_output_categ': fields.property(
528 relation='account.account',
529 string='Stock Output Account',
530 help="When doing real-time inventory valuation, counterpart journal items for all outgoing stock moves will be posted in this account, unless "
531 "there is a specific valuation account set on the destination location. This is the default value for all products in this category. It "
532 "can also directly be set on each product"),
533 'property_stock_valuation_account_id': fields.property(
535 relation='account.account',
536 string="Stock Valuation Account",
537 help="When real-time inventory valuation is enabled on a product, this account will hold the current value of the products.",),
541 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: