1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
22 from osv import fields, osv
23 from tools.translate import _
24 import decimal_precision as dp
26 class product_product(osv.osv):
27 _inherit = "product.product"
29 def get_product_accounts(self, cr, uid, product_id, context=None):
30 """ To get the stock input account, stock output account and stock journal related to product.
31 @param product_id: product id
32 @return: dictionary which contains information regarding stock input account, stock output account and stock journal
36 product_obj = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
38 stock_input_acc = product_obj.property_stock_account_input and product_obj.property_stock_account_input.id or False
39 if not stock_input_acc:
40 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
42 stock_output_acc = product_obj.property_stock_account_output and product_obj.property_stock_account_output.id or False
43 if not stock_output_acc:
44 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
46 journal_id = product_obj.categ_id.property_stock_journal and product_obj.categ_id.property_stock_journal.id or False
47 account_variation = product_obj.categ_id.property_stock_variation and product_obj.categ_id.property_stock_variation.id or False
50 'stock_account_input': stock_input_acc,
51 'stock_account_output': stock_output_acc,
52 'stock_journal': journal_id,
53 'property_stock_variation': account_variation
56 def do_change_standard_price(self, cr, uid, ids, datas, context=None):
57 """ Changes the Standard Price of Product and creates an account move accordingly.
58 @param datas : dict. contain default datas like new_price, stock_output_account, stock_input_account, stock_journal
59 @param context: A standard dictionary
63 location_obj = self.pool.get('stock.location')
64 move_obj = self.pool.get('account.move')
65 move_line_obj = self.pool.get('account.move.line')
69 new_price = datas.get('new_price', 0.0)
70 stock_output_acc = datas.get('stock_output_account', False)
71 stock_input_acc = datas.get('stock_input_account', False)
72 journal_id = datas.get('stock_journal', False)
73 product_obj=self.browse(cr, uid, ids, context=context)[0]
74 account_variation = product_obj.categ_id.property_stock_variation
75 account_variation_id = account_variation and account_variation.id or False
76 if not account_variation_id: raise osv.except_osv(_('Error!'), _('Variation Account is not specified for Product Category: %s') % (product_obj.categ_id.name))
78 loc_ids = location_obj.search(cr, uid,[('usage','=','internal')])
80 for location in location_obj.browse(cr, uid, loc_ids, context=context):
83 'location': location.id,
84 'compute_child': False
87 product = self.browse(cr, uid, rec_id, context=c)
88 qty = product.qty_available
89 diff = product.standard_price - new_price
90 if not diff: raise osv.except_osv(_('Error!'), _("Could not find any difference between standard price and new price!"))
92 company_id = location.company_id and location.company_id.id or False
93 if not company_id: raise osv.except_osv(_('Error!'), _('Company is not specified in Location'))
98 journal_id = product.categ_id.property_stock_journal and product.categ_id.property_stock_journal.id or False
100 raise osv.except_osv(_('Error!'),
101 _('There is no journal defined '\
102 'on the product category: "%s" (id: %d)') % \
103 (product.categ_id.name,
104 product.categ_id.id,))
105 move_id = move_obj.create(cr, uid, {
106 'journal_id': journal_id,
107 'company_id': company_id
110 move_ids.append(move_id)
114 if not stock_input_acc:
115 stock_input_acc = product.product_tmpl_id.\
116 property_stock_account_input.id
117 if not stock_input_acc:
118 stock_input_acc = product.categ_id.\
119 property_stock_account_input_categ.id
120 if not stock_input_acc:
121 raise osv.except_osv(_('Error!'),
122 _('There is no stock input account defined ' \
123 'for this product: "%s" (id: %d)') % \
126 amount_diff = qty * diff
127 move_line_obj.create(cr, uid, {
128 'name': product.name,
129 'account_id': stock_input_acc,
130 'debit': amount_diff,
133 move_line_obj.create(cr, uid, {
134 'name': product.categ_id.name,
135 'account_id': account_variation_id,
136 'credit': amount_diff,
140 if not stock_output_acc:
141 stock_output_acc = product.product_tmpl_id.\
142 property_stock_account_output.id
143 if not stock_output_acc:
144 stock_output_acc = product.categ_id.\
145 property_stock_account_output_categ.id
146 if not stock_output_acc:
147 raise osv.except_osv(_('Error!'),
148 _('There is no stock output account defined ' \
149 'for this product: "%s" (id: %d)') % \
152 amount_diff = qty * -diff
153 move_line_obj.create(cr, uid, {
154 'name': product.name,
155 'account_id': stock_output_acc,
156 'credit': amount_diff,
159 move_line_obj.create(cr, uid, {
160 'name': product.categ_id.name,
161 'account_id': account_variation_id,
162 'debit': amount_diff,
166 self.write(cr, uid, rec_id, {'standard_price': new_price})
170 def view_header_get(self, cr, user, view_id, view_type, context=None):
173 res = super(product_product, self).view_header_get(cr, user, view_id, view_type, context)
175 if (context.get('active_id', False)) and (context.get('active_model') == 'stock.location'):
176 return _('Products: ')+self.pool.get('stock.location').browse(cr, user, context['active_id'], context).name
179 def get_product_available(self, cr, uid, ids, context=None):
180 """ Finds whether product is available or not in particular warehouse.
181 @return: Dictionary of values
185 states = context.get('states',[])
186 what = context.get('what',())
188 ids = self.search(cr, uid, [])
189 res = {}.fromkeys(ids, 0.0)
193 # TODO: write in more ORM way, less queries, more pg84 magic
194 if context.get('shop', False):
195 cr.execute('select warehouse_id from sale_shop where id=%s', (int(context['shop']),))
198 context['warehouse'] = res2[0]
200 if context.get('warehouse', False):
201 cr.execute('select lot_stock_id from stock_warehouse where id=%s', (int(context['warehouse']),))
204 context['location'] = res2[0]
206 if context.get('location', False):
207 if type(context['location']) == type(1):
208 location_ids = [context['location']]
209 elif type(context['location']) in (type(''), type(u'')):
210 location_ids = self.pool.get('stock.location').search(cr, uid, [('name','ilike',context['location'])], context=context)
212 location_ids = context['location']
215 wids = self.pool.get('stock.warehouse').search(cr, uid, [], context=context)
216 for w in self.pool.get('stock.warehouse').browse(cr, uid, wids, context=context):
217 location_ids.append(w.lot_stock_id.id)
219 # build the list of ids of children of the location given by id
220 if context.get('compute_child',True):
221 child_location_ids = self.pool.get('stock.location').search(cr, uid, [('location_id', 'child_of', location_ids)])
222 location_ids = child_location_ids or location_ids
224 location_ids = location_ids
229 for product in self.read(cr, uid, ids, ['uom_id'], context=context):
230 product_uom_id = product['uom_id'][0] # uom id is required!
231 product2uom[product['id']] = product_uom_id
232 uoms_ids[product_uom_id] = True
234 uom_obj = self.pool.get('product.uom')
235 for uom in uom_obj.browse(cr, uid, uoms_ids.keys(), context=context):
241 from_date = context.get('from_date',False)
242 to_date = context.get('to_date',False)
245 where = [tuple(location_ids),tuple(location_ids),tuple(ids),tuple(states)]
246 if from_date and to_date:
247 date_str = "date>=%s and date<=%s"
248 where.append(tuple([from_date]))
249 where.append(tuple([to_date]))
251 date_str = "date>=%s"
252 date_values = [from_date]
254 date_str = "date<=%s"
255 date_values = [to_date]
257 prodlot_id = context.get('prodlot_id', False)
259 # TODO: perhaps merge in one query.
261 where.append(tuple(date_values))
263 # all moves from a location out of the set to a location in the set
265 'select sum(product_qty), product_id, product_uom '\
267 'where location_id NOT IN %s '\
268 'and location_dest_id IN %s '\
269 'and product_id IN %s '\
270 '' + (prodlot_id and ('and prodlot_id = ' + str(prodlot_id)) or '') + ' '\
271 'and state IN %s ' + (date_str and 'and '+date_str+' ' or '') +' '\
272 'group by product_id,product_uom',tuple(where))
273 results = cr.fetchall()
275 # all moves from a location in the set to a location out of the set
277 'select sum(product_qty), product_id, product_uom '\
279 'where location_id IN %s '\
280 'and location_dest_id NOT IN %s '\
281 'and product_id IN %s '\
282 '' + (prodlot_id and ('and prodlot_id = ' + str(prodlot_id)) or '') + ' '\
283 'and state in %s ' + (date_str and 'and '+date_str+' ' or '') + ' '\
284 'group by product_id,product_uom',tuple(where))
285 results2 = cr.fetchall()
286 uom_obj = self.pool.get('product.uom')
287 uoms = map(lambda x: x[2], results) + map(lambda x: x[2], results2)
288 if context.get('uom', False):
289 uoms += [context['uom']]
291 uoms = filter(lambda x: x not in uoms_o.keys(), uoms)
293 uoms = uom_obj.browse(cr, uid, list(set(uoms)), context=context)
296 #TOCHECK: before change uom of product, stock move line are in old uom.
297 context.update({'raise-exception': False})
298 for amount, prod_id, prod_uom in results:
299 amount = uom_obj._compute_qty_obj(cr, uid, uoms_o[prod_uom], amount,
300 uoms_o[context.get('uom', False) or product2uom[prod_id]], context=context)
301 res[prod_id] += amount
302 for amount, prod_id, prod_uom in results2:
303 amount = uom_obj._compute_qty_obj(cr, uid, uoms_o[prod_uom], amount,
304 uoms_o[context.get('uom', False) or product2uom[prod_id]], context=context)
305 res[prod_id] -= amount
308 def _product_available(self, cr, uid, ids, field_names=None, arg=False, context=None):
309 """ Finds the incoming and outgoing quantity of product.
310 @return: Dictionary of values
318 res[id] = {}.fromkeys(field_names, 0.0)
319 for f in field_names:
321 if f == 'qty_available':
322 c.update({ 'states': ('done',), 'what': ('in', 'out') })
323 if f == 'virtual_available':
324 c.update({ 'states': ('confirmed','waiting','assigned','done'), 'what': ('in', 'out') })
325 if f == 'incoming_qty':
326 c.update({ 'states': ('confirmed','waiting','assigned'), 'what': ('in',) })
327 if f == 'outgoing_qty':
328 c.update({ 'states': ('confirmed','waiting','assigned'), 'what': ('out',) })
329 stock = self.get_product_available(cr, uid, ids, context=c)
331 res[id][f] = stock.get(id, 0.0)
335 'qty_available': fields.function(_product_available, method=True, type='float', string='Real Stock', help="Current quantities of products in selected locations or all internal if none have been selected.", multi='qty_available', digits_compute=dp.get_precision('Product UoM')),
336 'virtual_available': fields.function(_product_available, method=True, type='float', string='Virtual Stock', help="Future stock for this product according to the selected locations or all internal if none have been selected. Computed as: Real Stock - Outgoing + Incoming.", multi='qty_available', digits_compute=dp.get_precision('Product UoM')),
337 'incoming_qty': fields.function(_product_available, method=True, type='float', string='Incoming', help="Quantities of products that are planned to arrive in selected locations or all internal if none have been selected.", multi='qty_available', digits_compute=dp.get_precision('Product UoM')),
338 'outgoing_qty': fields.function(_product_available, method=True, type='float', string='Outgoing', help="Quantities of products that are planned to leave in selected locations or all internal if none have been selected.", multi='qty_available', digits_compute=dp.get_precision('Product UoM')),
339 'track_production': fields.boolean('Track Manufacturing Lots' , help="Forces to specify a Production Lot for all moves containing this product and generated by a Manufacturing Order"),
340 'track_incoming': fields.boolean('Track Incoming Lots', help="Forces to specify a Production Lot for all moves containing this product and coming from a Supplier Location"),
341 'track_outgoing': fields.boolean('Track Outgoing Lots', help="Forces to specify a Production Lot for all moves containing this product and going to a Customer Location"),
342 'location_id': fields.dummy(string='Stock Location', relation='stock.location', type='many2one'),
343 'valuation':fields.selection([('manual_periodic', 'Periodical (manual)'),
344 ('real_time','Real Time (automated)'),], 'Inventory Valuation',
345 help="If real-time valuation is enabled for a product, the system will automatically write journal entries corresponding to stock moves." \
346 "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."
351 'valuation': lambda *a: 'manual_periodic',
354 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
355 res = super(product_product,self).fields_view_get(cr, uid, view_id, view_type, context, toolbar=toolbar, submenu=submenu)
358 if ('location' in context) and context['location']:
359 location_info = self.pool.get('stock.location').browse(cr, uid, context['location'])
360 fields=res.get('fields',{})
362 if location_info.usage == 'supplier':
363 if fields.get('virtual_available'):
364 res['fields']['virtual_available']['string'] = _('Future Receptions')
365 if fields.get('qty_available'):
366 res['fields']['qty_available']['string'] = _('Received Qty')
368 if location_info.usage == 'internal':
369 if fields.get('virtual_available'):
370 res['fields']['virtual_available']['string'] = _('Future Stock')
372 if location_info.usage == 'customer':
373 if fields.get('virtual_available'):
374 res['fields']['virtual_available']['string'] = _('Future Deliveries')
375 if fields.get('qty_available'):
376 res['fields']['qty_available']['string'] = _('Delivered Qty')
378 if location_info.usage == 'inventory':
379 if fields.get('virtual_available'):
380 res['fields']['virtual_available']['string'] = _('Future P&L')
381 if fields.get('qty_available'):
382 res['fields']['qty_available']['string'] = _('P&L Qty')
384 if location_info.usage == 'procurement':
385 if fields.get('virtual_available'):
386 res['fields']['virtual_available']['string'] = _('Future Qty')
387 if fields.get('qty_available'):
388 res['fields']['qty_available']['string'] = _('Unplanned Qty')
390 if location_info.usage == 'production':
391 if fields.get('virtual_available'):
392 res['fields']['virtual_available']['string'] = _('Future Productions')
393 if fields.get('qty_available'):
394 res['fields']['qty_available']['string'] = _('Produced Qty')
399 class product_template(osv.osv):
400 _name = 'product.template'
401 _inherit = 'product.template'
403 'property_stock_procurement': fields.property(
406 relation='stock.location',
407 string="Procurement Location",
410 domain=[('usage','like','procurement')],
411 help="For the current product, this stock location will be used, instead of the default one, as the source location for stock moves generated by procurements"),
412 'property_stock_production': fields.property(
415 relation='stock.location',
416 string="Production Location",
419 domain=[('usage','like','production')],
420 help="For the current product, this stock location will be used, instead of the default one, as the source location for stock moves generated by production orders"),
421 'property_stock_inventory': fields.property(
424 relation='stock.location',
425 string="Inventory Location",
428 domain=[('usage','like','inventory')],
429 help="For the current product, this stock location will be used, instead of the default one, as the source location for stock moves generated when you do an inventory"),
430 'property_stock_account_input': fields.property('account.account',
431 type='many2one', relation='account.account',
432 string='Stock Input Account', method=True, view_load=True,
433 help='When doing real-time inventory valuation, counterpart Journal Items for all incoming stock moves will be posted in this account. If not set on the product, the one from the product category is used.'),
434 'property_stock_account_output': fields.property('account.account',
435 type='many2one', relation='account.account',
436 string='Stock Output Account', method=True, view_load=True,
437 help='When doing real-time inventory valuation, counterpart Journal Items for all outgoing stock moves will be posted in this account. If not set on the product, the one from the product category is used.'),
442 class product_category(osv.osv):
444 _inherit = 'product.category'
446 'property_stock_journal': fields.property('account.journal',
447 relation='account.journal', type='many2one',
448 string='Stock journal', method=True, view_load=True,
449 help="When doing real-time inventory valuation, this is the Accounting Journal in which entries will be automatically posted when stock moves are processed."),
450 'property_stock_account_input_categ': fields.property('account.account',
451 type='many2one', relation='account.account',
452 string='Stock Input Account', method=True, view_load=True,
453 help='When doing real-time inventory valuation, counterpart Journal Items for all incoming stock moves will be posted in this account. This is the default value for all products in this category, it can also directly be set on each product.'),
454 'property_stock_account_output_categ': fields.property('account.account',
455 type='many2one', relation='account.account',
456 string='Stock Output Account', method=True, view_load=True,
457 help='When doing real-time inventory valuation, counterpart Journal Items for all outgoing stock moves will be posted in this account. This is the default value for all products in this category, it can also directly be set on each product.'),
458 'property_stock_variation': fields.property('account.account',
460 relation='account.account',
461 string="Stock Variation Account",
462 method=True, view_load=True,
463 help="When real-time inventory valuation is enabled on a product, this account will hold the current value of the products.",),
468 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: