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={}):
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')
67 new_price = datas.get('new_price', 0.0)
68 stock_output_acc = datas.get('stock_output_account', False)
69 stock_input_acc = datas.get('stock_input_account', False)
70 journal_id = datas.get('stock_journal', False)
71 product_obj=self.browse(cr,uid,ids)[0]
72 account_variation = product_obj.categ_id.property_stock_variation
73 account_variation_id = account_variation and account_variation.id or False
74 if not account_variation_id: raise osv.except_osv(_('Error!'), _('Variation Account is not specified for Product Category: %s' % (product_obj.categ_id.name)))
76 loc_ids = location_obj.search(cr, uid,[('usage','=','internal')])
78 for location in location_obj.browse(cr, uid, loc_ids):
81 'location': location.id,
82 'compute_child': False
85 product = self.browse(cr, uid, rec_id, context=c)
86 qty = product.qty_available
87 diff = product.standard_price - new_price
88 if not diff: raise osv.except_osv(_('Error!'), _("Could not find any difference between standard price and new price!"))
90 company_id = location.company_id and location.company_id.id or False
91 if not company_id: raise osv.except_osv(_('Error!'), _('Company is not specified in Location'))
96 journal_id = product.categ_id.property_stock_journal and product.categ_id.property_stock_journal.id or False
98 raise osv.except_osv(_('Error!'),
99 _('There is no journal defined '\
100 'on the product category: "%s" (id: %d)') % \
101 (product.categ_id.name,
102 product.categ_id.id,))
103 move_id = move_obj.create(cr, uid, {
104 'journal_id': journal_id,
105 'company_id': company_id
108 move_ids.append(move_id)
112 if not stock_input_acc:
113 stock_input_acc = product.product_tmpl_id.\
114 property_stock_account_input.id
115 if not stock_input_acc:
116 stock_input_acc = product.categ_id.\
117 property_stock_account_input_categ.id
118 if not stock_input_acc:
119 raise osv.except_osv(_('Error!'),
120 _('There is no stock input account defined ' \
121 'for this product: "%s" (id: %d)') % \
124 amount_diff = qty * diff
125 move_line_obj.create(cr, uid, {
126 'name': product.name,
127 'account_id': stock_input_acc,
128 'debit': amount_diff,
131 move_line_obj.create(cr, uid, {
132 'name': product.categ_id.name,
133 'account_id': account_variation_id,
134 'credit': amount_diff,
138 if not stock_output_acc:
139 stock_output_acc = product.product_tmpl_id.\
140 property_stock_account_output.id
141 if not stock_output_acc:
142 stock_output_acc = product.categ_id.\
143 property_stock_account_output_categ.id
144 if not stock_output_acc:
145 raise osv.except_osv(_('Error!'),
146 _('There is no stock output account defined ' \
147 'for this product: "%s" (id: %d)') % \
150 amount_diff = qty * -diff
151 move_line_obj.create(cr, uid, {
152 'name': product.name,
153 'account_id': stock_output_acc,
154 'credit': amount_diff,
157 move_line_obj.create(cr, uid, {
158 'name': product.categ_id.name,
159 'account_id': account_variation_id,
160 'debit': amount_diff,
164 self.write(cr, uid, rec_id, {'standard_price': new_price})
168 def view_header_get(self, cr, user, view_id, view_type, context=None):
171 res = super(product_product, self).view_header_get(cr, user, view_id, view_type, context)
173 if (context.get('active_id', False)) and (context.get('active_model') == 'stock.location'):
174 return _('Products: ')+self.pool.get('stock.location').browse(cr, user, context['active_id'], context).name
177 def get_product_available(self, cr, uid, ids, context=None):
178 """ Finds whether product is available or not in particular warehouse.
179 @return: Dictionary of values
183 states = context.get('states',[])
184 what = context.get('what',())
186 ids = self.search(cr, uid, [])
187 res = {}.fromkeys(ids, 0.0)
191 # TODO: write in more ORM way, less queries, more pg84 magic
192 if context.get('shop', False):
193 cr.execute('select warehouse_id from sale_shop where id=%s', (int(context['shop']),))
196 context['warehouse'] = res2[0]
198 if context.get('warehouse', False):
199 cr.execute('select lot_stock_id from stock_warehouse where id=%s', (int(context['warehouse']),))
202 context['location'] = res2[0]
204 if context.get('location', False):
205 if type(context['location']) == type(1):
206 location_ids = [context['location']]
207 elif type(context['location']) in (type(''), type(u'')):
208 location_ids = self.pool.get('stock.location').search(cr, uid, [('name','ilike',context['location'])], context=context)
210 location_ids = context['location']
213 wids = self.pool.get('stock.warehouse').search(cr, uid, [], context=context)
214 for w in self.pool.get('stock.warehouse').browse(cr, uid, wids, context=context):
215 location_ids.append(w.lot_stock_id.id)
217 # build the list of ids of children of the location given by id
218 if context.get('compute_child',True):
219 child_location_ids = self.pool.get('stock.location').search(cr, uid, [('location_id', 'child_of', location_ids)])
220 location_ids = child_location_ids or location_ids
222 location_ids = location_ids
226 for product in self.browse(cr, uid, ids, context=context):
227 product2uom[product.id] = product.uom_id.id
228 uoms_o[product.uom_id.id] = product.uom_id
233 from_date = context.get('from_date',False)
234 to_date = context.get('to_date',False)
237 where = [tuple(location_ids),tuple(location_ids),tuple(ids),tuple(states)]
238 if from_date and to_date:
239 date_str = "date>=%s and date<=%s"
240 where.append(tuple([from_date]))
241 where.append(tuple([to_date]))
243 date_str = "date>=%s"
244 date_values = [from_date]
246 date_str = "date<=%s"
247 date_values = [to_date]
250 # TODO: perhaps merge in one query.
252 where.append(tuple(date_values))
254 # all moves from a location out of the set to a location in the set
256 'select sum(product_qty), product_id, product_uom '\
258 'where location_id NOT IN %s'\
259 'and location_dest_id IN %s'\
260 'and product_id IN %s'\
261 'and state IN %s' + (date_str and 'and '+date_str+' ' or '') +''\
262 'group by product_id,product_uom',tuple(where))
263 results = cr.fetchall()
265 # all moves from a location in the set to a location out of the set
267 'select sum(product_qty), product_id, product_uom '\
269 'where location_id IN %s'\
270 'and location_dest_id NOT IN %s '\
271 'and product_id IN %s'\
272 'and state in %s' + (date_str and 'and '+date_str+' ' or '') + ''\
273 'group by product_id,product_uom',tuple(where))
274 results2 = cr.fetchall()
275 uom_obj = self.pool.get('product.uom')
276 uoms = map(lambda x: x[2], results) + map(lambda x: x[2], results2)
277 if context.get('uom', False):
278 uoms += [context['uom']]
280 uoms = filter(lambda x: x not in uoms_o.keys(), uoms)
282 uoms = uom_obj.browse(cr, uid, list(set(uoms)), context=context)
285 for amount, prod_id, prod_uom in results:
286 amount = uom_obj._compute_qty_obj(cr, uid, uoms_o[prod_uom], amount,
287 uoms_o[context.get('uom', False) or product2uom[prod_id]])
288 res[prod_id] += amount
289 for amount, prod_id, prod_uom in results2:
290 amount = uom_obj._compute_qty_obj(cr, uid, uoms_o[prod_uom], amount,
291 uoms_o[context.get('uom', False) or product2uom[prod_id]])
292 res[prod_id] -= amount
295 def _product_available(self, cr, uid, ids, field_names=None, arg=False, context=None):
296 """ Finds the incoming and outgoing quantity of product.
297 @return: Dictionary of values
305 res[id] = {}.fromkeys(field_names, 0.0)
306 for f in field_names:
308 if f == 'qty_available':
309 c.update({ 'states': ('done',), 'what': ('in', 'out') })
310 if f == 'virtual_available':
311 c.update({ 'states': ('confirmed','waiting','assigned','done'), 'what': ('in', 'out') })
312 if f == 'incoming_qty':
313 c.update({ 'states': ('confirmed','waiting','assigned'), 'what': ('in',) })
314 if f == 'outgoing_qty':
315 c.update({ 'states': ('confirmed','waiting','assigned'), 'what': ('out',) })
316 stock = self.get_product_available(cr, uid, ids, context=c)
318 res[id][f] = stock.get(id, 0.0)
322 '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')),
323 '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')),
324 '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')),
325 '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')),
326 '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"),
327 '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"),
328 '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"),
329 'location_id': fields.dummy(string='Stock Location', relation='stock.location', type='many2one'),
330 'valuation':fields.selection([('manual_periodic', 'Periodical (manual)'),
331 ('real_time','Real Time (automated)'),], 'Inventory Valuation',
332 help="If real-time valuation is enabled for a product, the system will automatically write journal entries corresponding to stock moves." \
333 "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."
338 'valuation': lambda *a: 'manual_periodic',
341 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
342 res = super(product_product,self).fields_view_get(cr, uid, view_id, view_type, context, toolbar=toolbar, submenu=submenu)
345 if ('location' in context) and context['location']:
346 location_info = self.pool.get('stock.location').browse(cr, uid, context['location'])
347 fields=res.get('fields',{})
349 if location_info.usage == 'supplier':
350 if fields.get('virtual_available'):
351 res['fields']['virtual_available']['string'] = _('Future Receptions')
352 if fields.get('qty_available'):
353 res['fields']['qty_available']['string'] = _('Received Qty')
355 if location_info.usage == 'internal':
356 if fields.get('virtual_available'):
357 res['fields']['virtual_available']['string'] = _('Future Stock')
359 if location_info.usage == 'customer':
360 if fields.get('virtual_available'):
361 res['fields']['virtual_available']['string'] = _('Future Deliveries')
362 if fields.get('qty_available'):
363 res['fields']['qty_available']['string'] = _('Delivered Qty')
365 if location_info.usage == 'inventory':
366 if fields.get('virtual_available'):
367 res['fields']['virtual_available']['string'] = _('Future P&L')
368 if fields.get('qty_available'):
369 res['fields']['qty_available']['string'] = _('P&L Qty')
371 if location_info.usage == 'procurement':
372 if fields.get('virtual_available'):
373 res['fields']['virtual_available']['string'] = _('Future Qty')
374 if fields.get('qty_available'):
375 res['fields']['qty_available']['string'] = _('Unplanned Qty')
377 if location_info.usage == 'production':
378 if fields.get('virtual_available'):
379 res['fields']['virtual_available']['string'] = _('Future Productions')
380 if fields.get('qty_available'):
381 res['fields']['qty_available']['string'] = _('Produced Qty')
386 class product_template(osv.osv):
387 _name = 'product.template'
388 _inherit = 'product.template'
390 'property_stock_procurement': fields.property(
393 relation='stock.location',
394 string="Procurement Location",
397 domain=[('usage','like','procurement')],
398 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"),
399 'property_stock_production': fields.property(
402 relation='stock.location',
403 string="Production Location",
406 domain=[('usage','like','production')],
407 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"),
408 'property_stock_inventory': fields.property(
411 relation='stock.location',
412 string="Inventory Location",
415 domain=[('usage','like','inventory')],
416 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"),
417 'property_stock_account_input': fields.property('account.account',
418 type='many2one', relation='account.account',
419 string='Stock Input Account', method=True, view_load=True,
420 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.'),
421 'property_stock_account_output': fields.property('account.account',
422 type='many2one', relation='account.account',
423 string='Stock Output Account', method=True, view_load=True,
424 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.'),
429 class product_category(osv.osv):
431 _inherit = 'product.category'
433 'property_stock_journal': fields.property('account.journal',
434 relation='account.journal', type='many2one',
435 string='Stock journal', method=True, view_load=True,
436 help="When doing real-time inventory valuation, this is the Accounting Journal in which entries will be automatically posted when stock moves are processed."),
437 'property_stock_account_input_categ': fields.property('account.account',
438 type='many2one', relation='account.account',
439 string='Stock Input Account', method=True, view_load=True,
440 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.'),
441 'property_stock_account_output_categ': fields.property('account.account',
442 type='many2one', relation='account.account',
443 string='Stock Output Account', method=True, view_load=True,
444 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.'),
445 'property_stock_variation': fields.property('account.account',
447 relation='account.account',
448 string="Stock Variation Account",
449 method=True, view_load=True,
450 help="When real-time inventory valuation is enabled on a product, this account will hold the current value of the products.",),
455 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: