ef7bfb14ea290cd92842f907d22d980808de5fd1
[odoo/odoo.git] / addons / stock / product.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
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.
11 #
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.
16 #
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/>.
19 #
20 ##############################################################################
21
22 from osv import fields, osv
23 from tools.translate import _
24
25
26 class product_product(osv.osv):
27     _inherit = "product.product"
28
29     def do_change_standard_price(self, cr, uid, ids, datas, context={}):
30         """ 
31              Changes the Standard Price of Product. 
32              And creates an account move accordingly.
33             
34              @param self: The object pointer.
35              @param cr: A database cursor
36              @param uid: ID of the user currently logged in
37              @param ids: List of IDs selected 
38              @param datas : dict. contain default datas like new_price, stock_output_account, stock_input_account, stock_journal
39              @param context: A standard dictionary 
40              
41              @return:  
42         
43         """        
44         location_obj = self.pool.get('stock.location')
45         move_obj = self.pool.get('account.move')
46         move_line_obj = self.pool.get('account.move.line')        
47         loc_ids = location_obj.search(cr, uid, [('account_id','<>',False),('usage','=','internal')])
48         
49         new_price = datas.get('new_price', 0.0)
50         stock_output_acc = datas.get('stock_output_account', False)
51         stock_input_acc = datas.get('stock_input_account', False)
52         journal_id = datas.get('stock_journal', False)
53
54         move_ids = []
55         for rec_id in ids:
56             for location in location_obj.browse(cr, uid, loc_ids):
57                 c = context.copy()
58                 c.update({
59                     'location': location.id,
60                     'compute_child': False
61                 })           
62                 
63                 product = self.browse(cr, uid, rec_id, context=c)
64                 qty = product.qty_available
65                 diff = product.standard_price - new_price                        
66                 assert diff, _("Could not find any difference between standard price and new price!")
67                 if qty:
68                     location_account = location.account_id and location.account_id.id or False
69                     company_id = location.company_id and location.company_id.id or False                
70                     assert location_account, _('Inventory Account is not specified for Location: %s' % (location.name))
71                     assert company_id, _('Company is not specified in Location')
72                     move_id = move_obj.create(cr, uid, {
73                                 'journal_id': journal_id, 
74                                 'company_id': company_id
75                                 }) 
76                     
77                     move_ids.append(move_id)
78                     if diff > 0:    
79                         amount_diff = qty * diff        
80                         move_line_obj.create(cr, uid, {
81                                     'name': product.name,
82                                     'account_id': stock_input_acc,
83                                     'debit': amount_diff,
84                                     'move_id': move_id,
85                                     })
86                         move_line_obj.create(cr, uid, {
87                                     'name': location.name,
88                                     'account_id': location_account,
89                                     'credit': amount_diff,
90                                     'move_id': move_id
91                                     })
92                     elif diff < 0: 
93                         amount_diff = qty * -diff
94                         move_line_obj.create(cr, uid, {
95                                     'name': product.name,
96                                     'account_id': stock_output_acc,
97                                     'credit': amount_diff,
98                                     'move_id': move_id
99                                     })
100                         move_line_obj.create(cr, uid, {
101                                     'name': location.name,
102                                     'account_id': location_account,
103                                     'debit': amount_diff,
104                                     'move_id': move_id
105                                     })                   
106                 
107             self.write(cr, uid, rec_id, {'standard_price': new_price})
108
109         return True
110
111     def view_header_get(self, cr, user, view_id, view_type, context):
112         res = super(product_product, self).view_header_get(cr, user, view_id, view_type, context)
113         if res: return res
114         if (context.get('location', False)):
115             return _('Products: ')+self.pool.get('stock.location').browse(cr, user, context['location'], context).name
116         return res
117
118     def get_product_available(self,cr,uid,ids,context=None):
119         if not context:
120             context = {}
121         states=context.get('states',[])
122         what=context.get('what',())
123         if not ids:
124             ids = self.search(cr, uid, [])
125         res = {}.fromkeys(ids, 0.0)
126         if not ids:
127             return res
128
129         if context.get('shop', False):
130             cr.execute('select warehouse_id from sale_shop where id=%s', (int(context['shop']),))
131             res2 = cr.fetchone()
132             if res2:
133                 context['warehouse'] = res2[0]
134
135         if context.get('warehouse', False):
136             cr.execute('select lot_stock_id from stock_warehouse where id=%s', (int(context['warehouse']),))
137             res2 = cr.fetchone()
138             if res2:
139                 context['location'] = res2[0]
140
141         if context.get('location', False):
142             if type(context['location']) == type(1):
143                 location_ids = [context['location']]
144             else:
145                 location_ids = context['location']
146         else:
147             location_ids = []
148             wids = self.pool.get('stock.warehouse').search(cr, uid, [], context=context)
149             for w in self.pool.get('stock.warehouse').browse(cr, uid, wids, context=context):
150                 location_ids.append(w.lot_stock_id.id)
151
152         # build the list of ids of children of the location given by id
153         if context.get('compute_child',True):
154             child_location_ids = self.pool.get('stock.location').search(cr, uid, [('location_id', 'child_of', location_ids)])
155             location_ids= len(child_location_ids) and child_location_ids or location_ids
156         else:
157             location_ids= location_ids
158
159         uoms_o = {}
160         product2uom = {}
161         for product in self.browse(cr, uid, ids, context=context):
162             product2uom[product.id] = product.uom_id.id
163             uoms_o[product.uom_id.id] = product.uom_id
164
165         results = []
166         results2 = []
167
168         from_date=context.get('from_date',False)
169         to_date=context.get('to_date',False)
170         date_str=False
171         if from_date and to_date:
172             date_str="date_planned>='%s' and date_planned<='%s'"%(from_date,to_date)
173         elif from_date:
174             date_str="date_planned>='%s'"%(from_date)
175         elif to_date:
176             date_str="date_planned<='%s'"%(to_date)
177
178         if 'in' in what:
179             # all moves from a location out of the set to a location in the set
180             cr.execute(
181                 'select sum(product_qty), product_id, product_uom '\
182                 'from stock_move '\
183                 'where location_id <> ANY(%s)'\
184                 'and location_dest_id =ANY(%s)'\
185                 'and product_id =ANY(%s)'\
186                 'and state in %s' + (date_str and 'and '+date_str+' ' or '') +''\
187                 'group by product_id,product_uom',(location_ids,location_ids,ids,tuple(states),)
188             )
189             results = cr.fetchall()
190         if 'out' in what:
191             # all moves from a location in the set to a location out of the set
192             cr.execute(
193                 'select sum(product_qty), product_id, product_uom '\
194                 'from stock_move '\
195                 'where location_id = ANY(%s)'\
196                 'and location_dest_id <> ANY(%s) '\
197                 'and product_id =ANY(%s)'\
198                 'and state in %s' + (date_str and 'and '+date_str+' ' or '') + ''\
199                 'group by product_id,product_uom',(location_ids,location_ids,ids,tuple(states),)
200             )
201             results2 = cr.fetchall()
202         uom_obj = self.pool.get('product.uom')
203         uoms = map(lambda x: x[2], results) + map(lambda x: x[2], results2)
204         if context.get('uom', False):
205             uoms += [context['uom']]
206
207         uoms = filter(lambda x: x not in uoms_o.keys(), uoms)
208         if uoms:
209             uoms = uom_obj.browse(cr, uid, list(set(uoms)), context=context)
210         for o in uoms:
211             uoms_o[o.id] = o
212         for amount, prod_id, prod_uom in results:
213             amount = uom_obj._compute_qty_obj(cr, uid, uoms_o[prod_uom], amount,
214                     uoms_o[context.get('uom', False) or product2uom[prod_id]])
215             res[prod_id] += amount
216         for amount, prod_id, prod_uom in results2:
217             amount = uom_obj._compute_qty_obj(cr, uid, uoms_o[prod_uom], amount,
218                     uoms_o[context.get('uom', False) or product2uom[prod_id]])
219             res[prod_id] -= amount
220         return res
221
222     def _product_available(self, cr, uid, ids, field_names=None, arg=False, context={}):
223         if not field_names:
224             field_names=[]
225         res = {}
226         for id in ids:
227             res[id] = {}.fromkeys(field_names, 0.0)
228         for f in field_names:
229             c = context.copy()
230             if f=='qty_available':
231                 c.update({ 'states':('done',), 'what':('in', 'out') })
232             if f=='virtual_available':
233                 c.update({ 'states':('confirmed','waiting','assigned','done'), 'what':('in', 'out') })
234             if f=='incoming_qty':
235                 c.update({ 'states':('confirmed','waiting','assigned'), 'what':('in',) })
236             if f=='outgoing_qty':
237                 c.update({ 'states':('confirmed','waiting','assigned'), 'what':('out',) })
238             stock=self.get_product_available(cr,uid,ids,context=c)
239             for id in ids:
240                 res[id][f] = stock.get(id, 0.0)
241         return res
242
243     _columns = {
244         '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'),
245         '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'),
246         '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'),
247         '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'),
248         'track_production': fields.boolean('Track Production Lots' , help="Forces to use a Production Lot during production order"),
249         'track_incoming': fields.boolean('Track Incoming Lots', help="Forces to use a tracking lot during receptions"),
250         'track_outgoing': fields.boolean('Track Outgoing Lots', help="Forces to use a tracking lot during deliveries"),
251         'location_id': fields.dummy(string='Location', relation='stock.location', type='many2one', domain=[('usage','=','internal')]),
252     }
253     def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
254         res = super(product_product,self).fields_view_get(cr, uid, view_id, view_type, context, toolbar=toolbar, submenu=submenu)
255         if context == None:
256             context = {}
257         if ('location' in context) and context['location']:
258             location_info = self.pool.get('stock.location').browse(cr, uid, context['location'])
259             fields=res.get('fields',{})
260             if fields:
261                 if location_info.usage == 'supplier':
262                     if fields.get('virtual_available'):
263                         res['fields']['virtual_available']['string'] = _('Future Receptions')
264                     if fields.get('qty_available'):
265                         res['fields']['qty_available']['string'] = _('Received Qty')
266
267                 if location_info.usage == 'internal':
268                     if fields.get('virtual_available'):
269                         res['fields']['virtual_available']['string'] = _('Future Stock')
270
271                 if location_info.usage == 'customer':
272                     if fields.get('virtual_available'):
273                         res['fields']['virtual_available']['string'] = _('Future Deliveries')
274                     if fields.get('qty_available'):
275                         res['fields']['qty_available']['string'] = _('Delivered Qty')
276
277                 if location_info.usage == 'inventory':
278                     if fields.get('virtual_available'):
279                         res['fields']['virtual_available']['string'] = _('Future P&L')
280                     res['fields']['qty_available']['string'] = _('P&L Qty')
281
282                 if location_info.usage == 'procurement':
283                     if fields.get('virtual_available'):
284                         res['fields']['virtual_available']['string'] = _('Future Qty')
285                     if fields.get('qty_available'):
286                         res['fields']['qty_available']['string'] = _('Unplanned Qty')
287
288                 if location_info.usage == 'production':
289                     if fields.get('virtual_available'):
290                         res['fields']['virtual_available']['string'] = _('Future Productions')
291                     if fields.get('qty_available'):
292                         res['fields']['qty_available']['string'] = _('Produced Qty')
293
294         return res
295 product_product()
296
297
298 class product_template(osv.osv):
299     _name = 'product.template'
300     _inherit = 'product.template'
301     _columns = {
302         'property_stock_procurement': fields.property(
303             'stock.location',
304             type='many2one',
305             relation='stock.location',
306             string="Requisition Location",
307             method=True,
308             view_load=True,
309             domain=[('usage','like','procurement')],
310             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"),
311         'property_stock_production': fields.property(
312             'stock.location',
313             type='many2one',
314             relation='stock.location',
315             string="Production Location",
316             method=True,
317             view_load=True,
318             domain=[('usage','like','production')],
319             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"),
320         'property_stock_inventory': fields.property(
321             'stock.location',
322             type='many2one',
323             relation='stock.location',
324             string="Inventory Location",
325             method=True,
326             view_load=True,
327             domain=[('usage','like','inventory')],
328             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"),
329         'property_stock_account_input': fields.property('account.account',
330             type='many2one', relation='account.account',
331             string='Stock Input Account', method=True, view_load=True,
332             help='This account will be used, instead of the default one, to value input stock'),
333         'property_stock_account_output': fields.property('account.account',
334             type='many2one', relation='account.account',
335             string='Stock Output Account', method=True, view_load=True,
336             help='This account will be used, instead of the default one, to value output stock'),
337     }
338
339 product_template()
340
341
342 class product_category(osv.osv):
343     _inherit = 'product.category'
344     _columns = {
345         'property_stock_journal': fields.property('account.journal',
346             relation='account.journal', type='many2one',
347             string='Stock journal', method=True, view_load=True,
348             help="This journal will be used for the accounting move generated by stock move"),
349         'property_stock_account_input_categ': fields.property('account.account',
350             type='many2one', relation='account.account',
351             string='Stock Input Account', method=True, view_load=True,
352             help='This account will be used to value the input stock'),
353         'property_stock_account_output_categ': fields.property('account.account',
354             type='many2one', relation='account.account',
355             string='Stock Output Account', method=True, view_load=True,
356             help='This account will be used to value the output stock'),
357     }
358
359 product_category()
360
361 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
362