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 from openerp.tools.safe_eval import safe_eval as eval
25 import openerp.addons.decimal_precision as dp
27 class product_product(osv.osv):
28 _inherit = "product.product"
30 def _stock_move_count(self, cr, uid, ids, field_name, arg, context=None):
31 res = dict([(id, {'reception_count': 0, 'delivery_count': 0}) for id in ids])
32 move_pool=self.pool.get('stock.move')
33 moves = move_pool.read_group(cr, uid, [
34 ('product_id', 'in', ids),
35 ('location_id.usage', '!=', 'internal'),
36 ('location_dest_id.usage', '=', 'internal'),
37 ('state','in',('confirmed','assigned','pending'))
38 ], ['product_id'], ['product_id'])
40 product_id = move['product_id'][0]
41 res[product_id]['reception_count'] = move['product_id_count']
42 moves = move_pool.read_group(cr, uid, [
43 ('product_id', 'in', ids),
44 ('location_id.usage', '=', 'internal'),
45 ('location_dest_id.usage', '!=', 'internal'),
46 ('state','in',('confirmed','assigned','pending'))
47 ], ['product_id'], ['product_id'])
49 product_id = move['product_id'][0]
50 res[product_id]['delivery_count'] = move['product_id_count']
53 def view_header_get(self, cr, user, view_id, view_type, context=None):
56 res = super(product_product, self).view_header_get(cr, user, view_id, view_type, context)
58 if (context.get('active_id', False)) and (context.get('active_model') == 'stock.location'):
59 return _('Products: ')+self.pool.get('stock.location').browse(cr, user, context['active_id'], context).name
62 def _get_domain_locations(self, cr, uid, ids, context=None):
64 Parses the context and returns a list of location_ids based on it.
65 It will return all stock locations when no parameters are given
66 Possible parameters are shop, warehouse, location, force_company, compute_child
68 context = context or {}
70 location_obj = self.pool.get('stock.location')
71 warehouse_obj = self.pool.get('stock.warehouse')
74 if context.get('location', False):
75 if type(context['location']) == type(1):
76 location_ids = [context['location']]
77 elif type(context['location']) in (type(''), type(u'')):
78 domain = [('complete_name','ilike',context['location'])]
79 if context.get('force_company', False):
80 domain += [('company_id', '=', context['force_company'])]
81 location_ids = location_obj.search(cr, uid, domain, context=context)
83 location_ids = context['location']
85 if context.get('warehouse', False):
86 wids = [context['warehouse']]
88 wids = warehouse_obj.search(cr, uid, [], context=context)
90 for w in warehouse_obj.browse(cr, uid, wids, context=context):
91 location_ids.append(w.view_location_id.id)
93 operator = context.get('compute_child', True) and 'child_of' or 'in'
94 domain = context.get('force_company', False) and ['&', ('company_id', '=', context['force_company'])] or []
96 domain + [('location_id', operator, location_ids)],
97 domain + ['&', ('location_dest_id', operator, location_ids), '!', ('location_id', operator, location_ids)],
98 domain + ['&', ('location_id', operator, location_ids), '!', ('location_dest_id', operator, location_ids)]
101 def _get_domain_dates(self, cr, uid, ids, context):
102 from_date = context.get('from_date', False)
103 to_date = context.get('to_date', False)
106 domain.append(('date', '>=', from_date))
108 domain.append(('date', '<=', to_date))
111 def _product_available(self, cr, uid, ids, field_names=None, arg=False, context=None):
112 context = context or {}
113 field_names = field_names or []
115 domain_products = [('product_id', 'in', ids)]
116 domain_quant, domain_move_in, domain_move_out = self._get_domain_locations(cr, uid, ids, context=context)
117 domain_move_in += self._get_domain_dates(cr, uid, ids, context=context) + [('state', 'not in', ('done', 'cancel'))] + domain_products
118 domain_move_out += self._get_domain_dates(cr, uid, ids, context=context) + [('state', 'not in', ('done', 'cancel'))] + domain_products
119 domain_quant += domain_products
120 if context.get('lot_id') or context.get('owner_id') or context.get('package_id'):
121 if context.get('lot_id'):
122 domain_quant.append(('lot_id', '=', context['lot_id']))
123 if context.get('owner_id'):
124 domain_quant.append(('owner_id', '=', context['owner_id']))
125 if context.get('package_id'):
126 domain_quant.append(('package_id', '=', context['package_id']))
130 moves_in = self.pool.get('stock.move').read_group(cr, uid, domain_move_in, ['product_id', 'product_qty'], ['product_id'], context=context)
131 moves_out = self.pool.get('stock.move').read_group(cr, uid, domain_move_out, ['product_id', 'product_qty'], ['product_id'], context=context)
133 quants = self.pool.get('stock.quant').read_group(cr, uid, domain_quant, ['product_id', 'qty'], ['product_id'], context=context)
134 quants = dict(map(lambda x: (x['product_id'][0], x['qty']), quants))
136 moves_in = dict(map(lambda x: (x['product_id'][0], x['product_qty']), moves_in))
137 moves_out = dict(map(lambda x: (x['product_id'][0], x['product_qty']), moves_out))
141 'qty_available': quants.get(id, 0.0),
142 'incoming_qty': moves_in.get(id, 0.0),
143 'outgoing_qty': moves_out.get(id, 0.0),
144 'virtual_available': quants.get(id, 0.0) + moves_in.get(id, 0.0) - moves_out.get(id, 0.0),
149 def _search_product_quantity(self, cr, uid, obj, name, domain, context):
151 for field, operator, value in domain:
152 #to prevent sql injections
153 assert field in ('qty_available', 'virtual_available', 'incoming_qty', 'outgoing_qty'), 'Invalid domain left operand'
154 assert operator in ('<', '>', '=', '<=', '>='), 'Invalid domain operator'
155 assert isinstance(value, (float, int)), 'Invalid domain right operand'
160 product_ids = self.search(cr, uid, [], context=context)
163 #TODO: use a query instead of this browse record which is probably making the too much requests, but don't forget
164 #the context that can be set with a location, an owner...
165 for element in self.browse(cr, uid, product_ids, context=context):
166 if eval(str(element[field]) + operator + str(value)):
167 ids.append(element.id)
168 res.append(('id', 'in', ids))
171 def _product_available_text(self, cr, uid, ids, field_names=None, arg=False, context=None):
173 for product in self.browse(cr, uid, ids, context=context):
174 res[product.id] = str(product.qty_available) + _(" In Stock")
178 'reception_count': fields.function(_stock_move_count, string="Reception", type='integer', multi='pickings'),
179 'delivery_count': fields.function(_stock_move_count, string="Delivery", type='integer', multi='pickings'),
180 'qty_in_stock': fields.function(_product_available_text, type='char'),
181 'qty_available': fields.function(_product_available, multi='qty_available',
182 type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
183 string='Quantity On Hand',
184 fnct_search=_search_product_quantity,
185 help="Current quantity of products.\n"
186 "In a context with a single Stock Location, this includes "
187 "goods stored at this Location, or any of its children.\n"
188 "In a context with a single Warehouse, this includes "
189 "goods stored in the Stock Location of this Warehouse, or any "
191 "stored in the Stock Location of the Warehouse of this Shop, "
192 "or any of its children.\n"
193 "Otherwise, this includes goods stored in any Stock Location "
194 "with 'internal' type."),
195 'virtual_available': fields.function(_product_available, multi='qty_available',
196 type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
197 string='Forecast Quantity',
198 fnct_search=_search_product_quantity,
199 help="Forecast quantity (computed as Quantity On Hand "
200 "- Outgoing + Incoming)\n"
201 "In a context with a single Stock Location, this includes "
202 "goods stored in this location, or any of its children.\n"
203 "In a context with a single Warehouse, this includes "
204 "goods stored in the Stock Location of this Warehouse, or any "
206 "Otherwise, this includes goods stored in any Stock Location "
207 "with 'internal' type."),
208 'incoming_qty': fields.function(_product_available, multi='qty_available',
209 type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
211 fnct_search=_search_product_quantity,
212 help="Quantity of products that are planned to arrive.\n"
213 "In a context with a single Stock Location, this includes "
214 "goods arriving to this Location, or any of its children.\n"
215 "In a context with a single Warehouse, this includes "
216 "goods arriving to the Stock Location of this Warehouse, or "
217 "any of its children.\n"
218 "Otherwise, this includes goods arriving to any Stock "
219 "Location with 'internal' type."),
220 'outgoing_qty': fields.function(_product_available, multi='qty_available',
221 type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
223 fnct_search=_search_product_quantity,
224 help="Quantity of products that are planned to leave.\n"
225 "In a context with a single Stock Location, this includes "
226 "goods leaving this Location, or any of its children.\n"
227 "In a context with a single Warehouse, this includes "
228 "goods leaving the Stock Location of this Warehouse, or "
229 "any of its children.\n"
230 "Otherwise, this includes goods leaving any Stock "
231 "Location with 'internal' type."),
232 'location_id': fields.dummy(string='Location', relation='stock.location', type='many2one'),
233 'warehouse_id': fields.dummy(string='Warehouse', relation='stock.warehouse', type='many2one'),
234 'orderpoint_ids': fields.one2many('stock.warehouse.orderpoint', 'product_id', 'Minimum Stock Rules'),
237 def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
238 res = super(product_product,self).fields_view_get(cr, uid, view_id, view_type, context, toolbar=toolbar, submenu=submenu)
241 if ('location' in context) and context['location']:
242 location_info = self.pool.get('stock.location').browse(cr, uid, context['location'])
243 fields=res.get('fields',{})
245 if location_info.usage == 'supplier':
246 if fields.get('virtual_available'):
247 res['fields']['virtual_available']['string'] = _('Future Receptions')
248 if fields.get('qty_available'):
249 res['fields']['qty_available']['string'] = _('Received Qty')
251 if location_info.usage == 'internal':
252 if fields.get('virtual_available'):
253 res['fields']['virtual_available']['string'] = _('Future Stock')
255 if location_info.usage == 'customer':
256 if fields.get('virtual_available'):
257 res['fields']['virtual_available']['string'] = _('Future Deliveries')
258 if fields.get('qty_available'):
259 res['fields']['qty_available']['string'] = _('Delivered Qty')
261 if location_info.usage == 'inventory':
262 if fields.get('virtual_available'):
263 res['fields']['virtual_available']['string'] = _('Future P&L')
264 if fields.get('qty_available'):
265 res['fields']['qty_available']['string'] = _('P&L Qty')
267 if location_info.usage == 'procurement':
268 if fields.get('virtual_available'):
269 res['fields']['virtual_available']['string'] = _('Future Qty')
270 if fields.get('qty_available'):
271 res['fields']['qty_available']['string'] = _('Unplanned Qty')
273 if location_info.usage == 'production':
274 if fields.get('virtual_available'):
275 res['fields']['virtual_available']['string'] = _('Future Productions')
276 if fields.get('qty_available'):
277 res['fields']['qty_available']['string'] = _('Produced Qty')
280 class product_template(osv.osv):
281 _name = 'product.template'
282 _inherit = 'product.template'
284 def _product_available(self, cr, uid, ids, name, arg, context=None):
285 res = dict.fromkeys(ids, 0)
286 for product in self.browse(cr, uid, ids, context=context):
288 # "reception_count": sum([p.reception_count for p in product.product_variant_ids]),
289 # "delivery_count": sum([p.delivery_count for p in product.product_variant_ids]),
290 "qty_available": sum([p.qty_available for p in product.product_variant_ids]),
291 "virtual_available": sum([p.virtual_available for p in product.product_variant_ids]),
292 "incoming_qty": sum([p.incoming_qty for p in product.product_variant_ids]),
293 "outgoing_qty": sum([p.outgoing_qty for p in product.product_variant_ids]),
297 def _search_product_quantity(self, cr, uid, obj, name, domain, context):
298 prod = self.pool.get("product.product")
300 for field, operator, value in domain:
301 #to prevent sql injections
302 assert field in ('qty_available', 'virtual_available', 'incoming_qty', 'outgoing_qty'), 'Invalid domain left operand'
303 assert operator in ('<', '>', '=', '<=', '>='), 'Invalid domain operator'
304 assert isinstance(value, (float, int)), 'Invalid domain right operand'
309 product_ids = prod.search(cr, uid, [], context=context)
312 #TODO: use a query instead of this browse record which is probably making the too much requests, but don't forget
313 #the context that can be set with a location, an owner...
314 for element in prod.browse(cr, uid, product_ids, context=context):
315 if eval(str(element[field]) + operator + str(value)):
316 ids.append(element.id)
317 res.append(('product_variant_ids', 'in', ids))
321 'valuation':fields.selection([('manual_periodic', 'Periodical (manual)'),
322 ('real_time','Real Time (automated)'),], 'Inventory Valuation',
323 help="If real-time valuation is enabled for a product, the system will automatically write journal entries corresponding to stock moves." \
324 "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."
326 'type': fields.selection([('product', 'Stockable Product'), ('consu', 'Consumable'), ('service', 'Service')], 'Product Type', required=True, help="Consumable: Will not imply stock management for this product. \nStockable product: Will imply stock management for this product."),
327 'property_stock_procurement': fields.property(
329 relation='stock.location',
330 string="Procurement Location",
331 domain=[('usage','like','procurement')],
332 help="This stock location will be used, instead of the default one, as the source location for stock moves generated by procurements."),
333 'property_stock_production': fields.property(
335 relation='stock.location',
336 string="Production Location",
337 domain=[('usage','like','production')],
338 help="This stock location will be used, instead of the default one, as the source location for stock moves generated by manufacturing orders."),
339 'property_stock_inventory': fields.property(
341 relation='stock.location',
342 string="Inventory Location",
343 domain=[('usage','like','inventory')],
344 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."),
345 '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."),
346 'loc_rack': fields.char('Rack', size=16),
347 'loc_row': fields.char('Row', size=16),
348 'loc_case': fields.char('Case', size=16),
349 '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"),
350 '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"),
351 'track_all': fields.boolean('Full Lots Traceability', help="Forces to specify a Serial Number on each and every operation related to this product"),
353 # sum of product variant qty
354 # 'reception_count': fields.function(_product_available, multi='qty_available',
355 # fnct_search=_search_product_quantity, type='float', string='Quantity On Hand'),
356 # 'delivery_count': fields.function(_product_available, multi='qty_available',
357 # fnct_search=_search_product_quantity, type='float', string='Quantity On Hand'),
358 'qty_available': fields.function(_product_available, multi='qty_available',
359 fnct_search=_search_product_quantity, type='float', string='Quantity On Hand'),
360 'virtual_available': fields.function(_product_available, multi='qty_available',
361 fnct_search=_search_product_quantity, type='float', string='Quantity Available'),
362 'incoming_qty': fields.function(_product_available, multi='qty_available',
363 fnct_search=_search_product_quantity, type='float', string='Incoming'),
364 'outgoing_qty': fields.function(_product_available, multi='qty_available',
365 fnct_search=_search_product_quantity, type='float', string='Outgoing'),
367 'route_ids': fields.many2many('stock.location.route', 'stock_route_product', 'product_id', 'route_id', 'Routes', domain="[('product_selectable', '=', True)]",
368 help="Depending on the modules installed, this will allow you to define the route of the product: whether it will be bought, manufactured, MTO/MTS,..."),
373 'valuation': 'manual_periodic',
376 def action_view_routes(self, cr, uid, ids, context=None):
377 route_obj = self.pool.get("stock.location.route")
378 act_obj = self.pool.get('ir.actions.act_window')
379 mod_obj = self.pool.get('ir.model.data')
380 product_route_ids = set()
381 for product in self.browse(cr, uid, ids, context=context):
382 product_route_ids |= set([r.id for r in product.route_ids])
383 product_route_ids |= set([r.id for r in product.categ_id.total_route_ids])
384 route_ids = route_obj.search(cr, uid, ['|', ('id', 'in', list(product_route_ids)), ('warehouse_selectable', '=', True)], context=context)
385 result = mod_obj.get_object_reference(cr, uid, 'stock', 'action_routes_form')
386 id = result and result[1] or False
387 result = act_obj.read(cr, uid, [id], context=context)[0]
388 result['domain'] = "[('id','in',[" + ','.join(map(str, route_ids)) + "])]"
391 class product_removal_strategy(osv.osv):
392 _name = 'product.removal'
393 _description = 'Removal Strategy'
396 'name': fields.char('Name', required=True),
397 'method': fields.char("Method", required=True, help="FIFO, LIFO..."),
401 class product_putaway_strategy(osv.osv):
402 _name = 'product.putaway'
403 _description = 'Put Away Strategy'
405 def _get_putaway_options(self, cr, uid, context=None):
406 return [('fixed', 'Fixed Location')]
409 'name': fields.char('Name', required=True),
410 'method': fields.selection(_get_putaway_options, "Method", required=True),
411 'fixed_location_ids': fields.one2many('stock.fixed.putaway.strat', 'putaway_id', 'Fixed Locations Per Product Category', help="When the method is fixed, this location will be used to store the products"),
418 def putaway_apply(self, cr, uid, putaway_strat, product, context=None):
419 if putaway_strat.method == 'fixed':
420 for strat in putaway_strat.fixed_location_ids:
421 categ = product.categ_id
423 if strat.category_id.id == categ.id:
424 return strat.fixed_location_id.id
425 categ = categ.parent_id
428 class fixed_putaway_strat(osv.osv):
429 _name = 'stock.fixed.putaway.strat'
432 'putaway_id': fields.many2one('product.putaway', 'Put Away Method', required=True),
433 'category_id': fields.many2one('product.category', 'Product Category', required=True),
434 'fixed_location_id': fields.many2one('stock.location', 'Location', required=True),
435 'sequence': fields.integer('Priority', help="Give to the more specialized category, a higher priority to have them in top of the list."),
439 class product_category(osv.osv):
440 _inherit = 'product.category'
442 def calculate_total_routes(self, cr, uid, ids, name, args, context=None):
444 for categ in self.browse(cr, uid, ids, context=context):
446 routes = [x.id for x in categ.route_ids]
447 while categ2.parent_id:
448 categ2 = categ2.parent_id
449 routes += [x.id for x in categ2.route_ids]
450 res[categ.id] = routes
454 'route_ids': fields.many2many('stock.location.route', 'stock_location_route_categ', 'categ_id', 'route_id', 'Routes', domain="[('product_categ_selectable', '=', True)]"),
455 'removal_strategy_id': fields.many2one('product.removal', 'Force Removal Strategy', help="Set a specific removal strategy that will be used regardless of the source location for this product category"),
456 'total_route_ids': fields.function(calculate_total_routes, relation='stock.location.route', type='many2many', string='Total routes', readonly=True),
460 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: