diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py
index 91a587fd14a..6d4c32cef2d 100644
--- a/addons/stock/models/product.py
+++ b/addons/stock/models/product.py
@@ -1,96 +1,173 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from openerp.osv import fields, osv
-from openerp.tools.translate import _
-from openerp.tools.safe_eval import safe_eval as eval
-import openerp.addons.decimal_precision as dp
-from openerp.tools.float_utils import float_round
-from openerp.exceptions import UserError
+from odoo import api, fields, models, _
+from odoo.addons import decimal_precision as dp
+from odoo.exceptions import UserError
+from odoo.tools.safe_eval import safe_eval as eval
+from odoo.tools.float_utils import float_round
-class product_product(osv.osv):
+class Product(models.Model):
_inherit = "product.product"
-
- def _stock_move_count(self, cr, uid, ids, field_name, arg, context=None):
- res = dict([(id, {'reception_count': 0, 'delivery_count': 0}) for id in ids])
- move_pool=self.pool.get('stock.move')
- moves = move_pool.read_group(cr, uid, [
- ('product_id', 'in', ids),
- ('location_id.usage', '!=', 'internal'),
- ('location_dest_id.usage', '=', 'internal'),
- ('state','in',('confirmed','assigned','pending'))
- ], ['product_id'], ['product_id'])
- for move in moves:
- product_id = move['product_id'][0]
- res[product_id]['reception_count'] = move['product_id_count']
- moves = move_pool.read_group(cr, uid, [
- ('product_id', 'in', ids),
- ('location_id.usage', '=', 'internal'),
- ('location_dest_id.usage', '!=', 'internal'),
- ('state','in',('confirmed','assigned','pending'))
- ], ['product_id'], ['product_id'])
- for move in moves:
- product_id = move['product_id'][0]
- res[product_id]['delivery_count'] = move['product_id_count']
+
+ reception_count = fields.Integer('Receipt', compute='_compute_reception_count')
+ delivery_count = fields.Integer('Delivery', compute='_compute_delivery_count')
+ stock_quant_ids = fields.One2many('stock.quant', 'product_id', help='Technical: used to compute quantities.')
+ stock_move_ids = fields.One2many('stock.move', 'product_id', help='Technical: used to compute quantities.')
+ qty_available = fields.Float(
+ 'Quantity On Hand', compute='_compute_quantities', search='_search_qty_available',
+ digits_compute=dp.get_precision('Product Unit of Measure'),
+ help="Current quantity of products.\n"
+ "In a context with a single Stock Location, this includes "
+ "goods stored at this Location, or any of its children.\n"
+ "In a context with a single Warehouse, this includes "
+ "goods stored in the Stock Location of this Warehouse, or any "
+ "of its children.\n"
+ "stored in the Stock Location of the Warehouse of this Shop, "
+ "or any of its children.\n"
+ "Otherwise, this includes goods stored in any Stock Location "
+ "with 'internal' type.")
+ virtual_available = fields.Float(
+ 'Forecast Quantity', compute='_compute_quantities', search='_search_virtual_available',
+ digits_compute=dp.get_precision('Product Unit of Measure'),
+ help="Forecast quantity (computed as Quantity On Hand "
+ "- Outgoing + Incoming)\n"
+ "In a context with a single Stock Location, this includes "
+ "goods stored in this location, or any of its children.\n"
+ "In a context with a single Warehouse, this includes "
+ "goods stored in the Stock Location of this Warehouse, or any "
+ "of its children.\n"
+ "Otherwise, this includes goods stored in any Stock Location "
+ "with 'internal' type.")
+ incoming_qty = fields.Float(
+ 'Incoming', compute='_compute_quantities', search='_search_incoming_qty',
+ digits_compute=dp.get_precision('Product Unit of Measure'),
+ help="Quantity of products that are planned to arrive.\n"
+ "In a context with a single Stock Location, this includes "
+ "goods arriving to this Location, or any of its children.\n"
+ "In a context with a single Warehouse, this includes "
+ "goods arriving to the Stock Location of this Warehouse, or "
+ "any of its children.\n"
+ "Otherwise, this includes goods arriving to any Stock "
+ "Location with 'internal' type.")
+ outgoing_qty = fields.Float(
+ 'Outgoing', compute='_compute_quantities', search='_search_outgoing_qty',
+ digits_compute=dp.get_precision('Product Unit of Measure'),
+ help="Quantity of products that are planned to leave.\n"
+ "In a context with a single Stock Location, this includes "
+ "goods leaving this Location, or any of its children.\n"
+ "In a context with a single Warehouse, this includes "
+ "goods leaving the Stock Location of this Warehouse, or "
+ "any of its children.\n"
+ "Otherwise, this includes goods leaving any Stock "
+ "Location with 'internal' type.")
+ # TDE CLEANME: unused except in one test
+ orderpoint_ids = fields.One2many('stock.warehouse.orderpoint', 'product_id', 'Minimum Stock Rules')
+ nbr_reordering_rules = fields.Integer('Reordering Rules', compute='_compute_nbr_reordering_rules')
+ reordering_min_qty = fields.Float(compute='_compute_nbr_reordering_rules')
+ reordering_max_qty = fields.Float(compute='_compute_nbr_reordering_rules')
+
+ @api.depends('stock_quant_ids', 'stock_move_ids')
+ def _compute_quantities(self):
+ res = self._compute_quantities_dict(self._context.get('lot_id'), self._context.get('owner_id'), self._context.get('package_id'), self._context.get('from_date'), self._context.get('to_date'))
+ for product in self:
+ product.qty_available = res[product.id]['qty_available']
+ product.incoming_qty = res[product.id]['incoming_qty']
+ product.outgoing_qty = res[product.id]['outgoing_qty']
+ product.virtual_available = res[product.id]['virtual_available']
+
+ @api.multi
+ def _product_available(self, field_names=None, arg=False):
+ """ Compatibility method """
+ return self._compute_quantities_dict(self._context.get('lot_id'), self._context.get('owner_id'), self._context.get('package_id'), self._context.get('from_date'), self._context.get('to_date'))
+
+ @api.multi
+ def _compute_quantities_dict(self, lot_id, owner_id, package_id, from_date=False, to_date=False):
+ domain_quant_loc, domain_move_in_loc, domain_move_out_loc = self._get_domain_locations()
+
+ domain_quant = [('product_id', 'in', self.ids)] + domain_quant_loc
+ domain_move_in = [('state', 'not in', ('done', 'cancel', 'draft')), ('product_id', 'in', self.ids)] + domain_move_in_loc
+ domain_move_out = [('state', 'not in', ('done', 'cancel', 'draft')), ('product_id', 'in', self.ids)] + domain_move_out_loc
+ if from_date:
+ domain_move_in += [('date', '>=', from_date)]
+ domain_move_out += [('date', '>=', from_date)]
+ if to_date:
+ domain_move_in += [('date', '<=', to_date)]
+ domain_move_out += [('date', '<=', to_date)]
+ if lot_id:
+ domain_quant += [('lot_id', '=', lot_id)]
+ if owner_id:
+ domain_quant += [('owner_id', '=', owner_id)]
+ domain_move_in += [('restrict_partner_id', '=', owner_id)]
+ domain_move_out += [('restrict_partner_id', '=', owner_id)]
+ if package_id:
+ domain_quant += [('package_id', '=', package_id)]
+
+ Move = self.env['stock.move']
+ Quant = self.env['stock.quant']
+ moves_in_res = dict((item['product_id'][0], item['product_qty']) for item in Move.read_group(domain_move_in, ['product_id', 'product_qty'], ['product_id']))
+ moves_out_res = dict((item['product_id'][0], item['product_qty']) for item in Move.read_group(domain_move_out, ['product_id', 'product_qty'], ['product_id']))
+ quants_res = dict((item['product_id'][0], item['qty']) for item in Quant.read_group(domain_quant, ['product_id', 'qty'], ['product_id']))
+
+ res = dict()
+ for product in self.with_context(prefetch_fields=False):
+ res[product.id] = {}
+ res[product.id]['qty_available'] = float_round(quants_res.get(product.id, 0.0), precision_rounding=product.uom_id.rounding)
+ res[product.id]['incoming_qty'] = float_round(moves_in_res.get(product.id, 0.0), precision_rounding=product.uom_id.rounding)
+ res[product.id]['outgoing_qty'] = float_round(moves_out_res.get(product.id, 0.0), precision_rounding=product.uom_id.rounding)
+ res[product.id]['virtual_available'] = float_round(
+ res[product.id]['qty_available'] + res[product.id]['incoming_qty'] - res[product.id]['outgoing_qty'],
+ precision_rounding=product.uom_id.rounding)
return res
- def view_header_get(self, cr, user, view_id, view_type, context=None):
- if context is None:
- context = {}
- res = super(product_product, self).view_header_get(cr, user, view_id, view_type, context)
- if res: return res
- if (context.get('active_id', False)) and (context.get('active_model') == 'stock.location'):
- return _('Products: ')+self.pool.get('stock.location').browse(cr, user, context['active_id'], context).name
- return res
-
- def _get_domain_locations(self, cr, uid, ids, context=None):
+ def _get_domain_locations(self):
'''
Parses the context and returns a list of location_ids based on it.
It will return all stock locations when no parameters are given
Possible parameters are shop, warehouse, location, force_company, compute_child
'''
- context = context or {}
-
- location_obj = self.pool.get('stock.location')
- warehouse_obj = self.pool.get('stock.warehouse')
+ # TDE FIXME: clean that brol, context seems overused
+ Warehouse = self.env['stock.warehouse']
location_ids = []
- if context.get('location', False):
- if isinstance(context['location'], (int, long)):
- location_ids = [context['location']]
- elif isinstance(context['location'], basestring):
- domain = [('complete_name','ilike',context['location'])]
- if context.get('force_company', False):
- domain += [('company_id', '=', context['force_company'])]
- location_ids = location_obj.search(cr, uid, domain, context=context)
+ if self.env.context.get('location', False):
+ if isinstance(self.env.context['location'], (int, long)):
+ location_ids = [self.env.context['location']]
+ elif isinstance(self.env.context['location'], basestring):
+ domain = [('complete_name', 'ilike', self.env.context['location'])]
+ if self.env.context.get('force_company', False):
+ domain += [('company_id', '=', self.env.context['force_company'])]
+ location_ids = self.env['stock.location'].search(domain).ids
else:
- location_ids = context['location']
+ location_ids = self.env.context['location']
else:
- if context.get('warehouse', False):
- if isinstance(context['warehouse'], (int, long)):
- wids = [context['warehouse']]
- elif isinstance(context['warehouse'], basestring):
- domain = [('name', 'ilike', context['warehouse'])]
- if context.get('force_company', False):
- domain += [('company_id', '=', context['force_company'])]
- wids = warehouse_obj.search(cr, uid, domain, context=context)
+ if self.env.context.get('warehouse', False):
+ if isinstance(self.env.context['warehouse'], (int, long)):
+ wids = [self.env.context['warehouse']]
+ elif isinstance(self.env.context['warehouse'], basestring):
+ domain = [('name', 'ilike', self.env.context['warehouse'])]
+ if self.env.context.get('force_company', False):
+ domain += [('company_id', '=', self.env.context['force_company'])]
+ wids = Warehouse.search(domain).ids
else:
- wids = context['warehouse']
+ wids = self.env.context['warehouse']
else:
- wids = warehouse_obj.search(cr, uid, [], context=context)
+ wids = Warehouse.search([]).ids
- for w in warehouse_obj.browse(cr, uid, wids, context=context):
+ for w in Warehouse.browse(wids):
location_ids.append(w.view_location_id.id)
+ return self._get_domain_locations_new(location_ids, company_id=self.env.context.get('force_company', False), compute_child=self.env.context.get('compute_child', True))
- operator = context.get('compute_child', True) and 'child_of' or 'in'
- domain = context.get('force_company', False) and ['&', ('company_id', '=', context['force_company'])] or []
- locations = location_obj.browse(cr, uid, location_ids, context=context)
+ def _get_domain_locations_new(self, location_ids, company_id=False, compute_child=True):
+ operator = compute_child and 'child_of' or 'in'
+ domain = company_id and ['&', ('company_id', '=', company_id)] or []
+ locations = self.env['stock.location'].browse(location_ids)
# TDE FIXME: should move the support of child_of + auto_join directly in expression
# The code has been modified because having one location with parent_left being
# 0 make the whole domain unusable
- hierarchical_locations = [location for location in locations if location.parent_left != 0 and operator == "child_of"]
- other_locations = [location for location in locations if location not in hierarchical_locations]
+ hierarchical_locations = locations.filtered(lambda location: location.parent_left != 0 and operator == "child_of")
+ other_locations = locations.filtered(lambda location: location not in hierarchical_locations) # TDE: set - set ?
loc_domain = []
dest_loc_domain = []
for location in hierarchical_locations:
@@ -113,258 +190,212 @@ class product_product(osv.osv):
domain + ['&'] + loc_domain + ['!'] + dest_loc_domain
)
- def _get_domain_dates(self, cr, uid, ids, context):
- from_date = context.get('from_date', False)
- to_date = context.get('to_date', False)
- domain = []
- if from_date:
- domain.append(('date', '>=', from_date))
- if to_date:
- domain.append(('date', '<=', to_date))
- return domain
+ def _search_virtual_available(self, operator, value):
+ # TDE FIXME: should probably clean the search methods
+ return self._search_product_quantity(operator, value, 'virtual_available')
- def _product_available(self, cr, uid, ids, field_names=None, arg=False, context=None):
- context = context or {}
- field_names = field_names or []
+ def _search_incoming_qty(self, operator, value):
+ # TDE FIXME: should probably clean the search methods
+ return self._search_product_quantity(operator, value, 'incoming_qty')
- domain_products = [('product_id', 'in', ids)]
- domain_quant, domain_move_in, domain_move_out = [], [], []
- domain_quant_loc, domain_move_in_loc, domain_move_out_loc = self._get_domain_locations(cr, uid, ids, context=context)
- domain_move_in += self._get_domain_dates(cr, uid, ids, context=context) + [('state', 'not in', ('done', 'cancel', 'draft'))] + domain_products
- domain_move_out += self._get_domain_dates(cr, uid, ids, context=context) + [('state', 'not in', ('done', 'cancel', 'draft'))] + domain_products
- domain_quant += domain_products
+ def _search_outgoing_qty(self, operator, value):
+ # TDE FIXME: should probably clean the search methods
+ return self._search_product_quantity(operator, value, 'outgoing_qty')
- if context.get('lot_id'):
- domain_quant.append(('lot_id', '=', context['lot_id']))
- if context.get('owner_id'):
- domain_quant.append(('owner_id', '=', context['owner_id']))
- owner_domain = ('restrict_partner_id', '=', context['owner_id'])
- domain_move_in.append(owner_domain)
- domain_move_out.append(owner_domain)
- if context.get('package_id'):
- domain_quant.append(('package_id', '=', context['package_id']))
+ def _search_product_quantity(self, operator, value, field):
+ # TDE FIXME: should probably clean the search methods
+ # to prevent sql injections
+ if field not in ('qty_available', 'virtual_available', 'incoming_qty', 'outgoing_qty'):
+ raise UserError('Invalid domain left operand')
+ if operator not in ('<', '>', '=', '!=', '<=', '>='):
+ raise UserError('Invalid domain operator')
+ if not isinstance(value, (float, int)):
+ raise UserError('Invalid domain right operand')
- domain_move_in += domain_move_in_loc
- domain_move_out += domain_move_out_loc
- moves_in = self.pool.get('stock.move').read_group(cr, uid, domain_move_in, ['product_id', 'product_qty'], ['product_id'], context=context)
- moves_out = self.pool.get('stock.move').read_group(cr, uid, domain_move_out, ['product_id', 'product_qty'], ['product_id'], context=context)
+ if operator == '=':
+ operator = '=='
- domain_quant += domain_quant_loc
- quants = self.pool.get('stock.quant').read_group(cr, uid, domain_quant, ['product_id', 'qty'], ['product_id'], context=context)
- quants = dict(map(lambda x: (x['product_id'][0], x['qty']), quants))
+ # TODO: Still optimization possible when searching virtual quantities
+ ids = []
+ for product in self.search([]):
+ if eval(str(product[field]) + operator + str(value)):
+ ids.append(product.id)
+ return [('id', 'in', ids)]
- moves_in = dict(map(lambda x: (x['product_id'][0], x['product_qty']), moves_in))
- moves_out = dict(map(lambda x: (x['product_id'][0], x['product_qty']), moves_out))
- res = {}
- ctx = context.copy()
- ctx.update({'prefetch_fields': False})
- for product in self.browse(cr, uid, ids, context=ctx):
- id = product.id
- qty_available = float_round(quants.get(id, 0.0), precision_rounding=product.uom_id.rounding)
- incoming_qty = float_round(moves_in.get(id, 0.0), precision_rounding=product.uom_id.rounding)
- outgoing_qty = float_round(moves_out.get(id, 0.0), precision_rounding=product.uom_id.rounding)
- virtual_available = float_round(quants.get(id, 0.0) + moves_in.get(id, 0.0) - moves_out.get(id, 0.0), precision_rounding=product.uom_id.rounding)
- res[id] = {
- 'qty_available': qty_available,
- 'incoming_qty': incoming_qty,
- 'outgoing_qty': outgoing_qty,
- 'virtual_available': virtual_available,
- }
- return res
+ def _search_qty_available(self, operator, value):
+ # TDE FIXME: should probably clean the search methods
+ if value == 0.0 and operator in ('==', '>=', '<='):
+ return self._search_product_quantity(operator, value, 'qty_available')
+ product_ids = self._search_qty_available_new(operator, value, self._context.get('lot_id'), self._context.get('owner_id'), self._context.get('package_id'))
+ return [('id', 'in', product_ids)]
- def _search_product_quantity(self, cr, uid, obj, name, domain, context):
- res = []
- for field, operator, value in domain:
- #to prevent sql injections
- assert field in ('qty_available', 'virtual_available', 'incoming_qty', 'outgoing_qty'), 'Invalid domain left operand'
- assert operator in ('<', '>', '=', '!=', '<=', '>='), 'Invalid domain operator'
- assert isinstance(value, (float, int)), 'Invalid domain right operand'
+ def _search_qty_available_new(self, operator, value, lot_id=False, owner_id=False, package_id=False):
+ # TDE FIXME: should probably clean the search methods
+ product_ids = set()
+ domain_quant = self._get_domain_locations()[0]
+ if lot_id:
+ domain_quant.append(('lot_id', '=', lot_id))
+ if owner_id:
+ domain_quant.append(('owner_id', '=', owner_id))
+ if package_id:
+ domain_quant.append(('package_id', '=', package_id))
+ quants_groupby = self.env['stock.quant'].read_group(domain_quant, ['product_id', 'qty'], ['product_id'])
+ for quant in quants_groupby:
+ if eval('%s %s %s' % (quant['qty'], operator, value)):
+ product_ids.add(quant['product_id'][0])
+ return list(product_ids)
- if operator == '=':
- operator = '=='
-
- ids = []
- if name == 'qty_available' and (value != 0.0 or operator not in ('==', '>=', '<=')):
- res.append(('id', 'in', self._search_qty_available(cr, uid, operator, value, context)))
- else:
- product_ids = self.search(cr, uid, [], context=context)
- if product_ids:
- #TODO: Still optimization possible when searching virtual quantities
- for element in self.browse(cr, uid, product_ids, context=context):
- if eval(str(element[field]) + operator + str(value)):
- ids.append(element.id)
- res.append(('id', 'in', ids))
- return res
-
- def _search_qty_available(self, cr, uid, operator, value, context):
- domain_quant = []
- if context.get('lot_id'):
- domain_quant.append(('lot_id', '=', context['lot_id']))
- if context.get('owner_id'):
- domain_quant.append(('owner_id', '=', context['owner_id']))
- if context.get('package_id'):
- domain_quant.append(('package_id', '=', context['package_id']))
- domain_quant += self._get_domain_locations(cr, uid, [], context=context)[0]
- quants = self.pool.get('stock.quant').read_group(cr, uid, domain_quant, ['product_id', 'qty'], ['product_id'], context=context)
- quants = dict(map(lambda x: (x['product_id'][0], x['qty']), quants))
- quants = dict((k, v) for k, v in quants.iteritems() if eval(str(v) + operator + str(value)))
- return(list(quants))
-
- def _compute_nbr_reordering_rules(self, cr, uid, ids, field_names=None, arg=None, context=None):
- res = dict.fromkeys(ids, {'nbr_reordering_rules': 0, 'reordering_min_qty': 0, 'reordering_max_qty': 0})
- product_data = self.pool['stock.warehouse.orderpoint'].read_group(cr, uid, [('product_id', 'in', ids)], ['product_id', 'product_min_qty', 'product_max_qty'], ['product_id'], context=context)
- for data in product_data:
+ def _compute_nbr_reordering_rules(self):
+ read_group_res = self.env['stock.warehouse.orderpoint'].read_group(
+ [('product_id', 'in', self.ids)],
+ ['product_id', 'product_min_qty', 'product_max_qty'],
+ ['product_id'])
+ res = dict.fromkeys(self.ids, dict())
+ for data in read_group_res:
res[data['product_id'][0]]['nbr_reordering_rules'] = int(data['product_id_count'])
res[data['product_id'][0]]['reordering_min_qty'] = data['product_min_qty']
res[data['product_id'][0]]['reordering_max_qty'] = data['product_max_qty']
+ for product in self:
+ product.nbr_reordering_rules = res[product.id].get('nbr_reordering_rules', 0)
+ product.reordering_min_qty = res[product.id].get('reordering_min_qty', 0)
+ product.reordering_max_qty = res[product.id].get('reordering_max_qty', 0)
+
+ @api.onchange('tracking')
+ def onchange_tracking(self):
+ products = self.filtered(lambda self: self.tracking and self.tracking != 'none')
+ if products:
+ unassigned_quants = self.env['stock.quant'].search_count([('product_id', 'in', products.ids), ('lot_id', '=', False), ('location_id.usage','=', 'internal')])
+ if unassigned_quants:
+ return {
+ 'warning': {
+ 'title': _('Warning!'),
+ 'message': _("You have products in stock that have no lot number. You can assign serial numbers by doing an inventory. ")}}
+
+ @api.model
+ def view_header_get(self, view_id, view_type):
+ res = super(Product, self).view_header_get(view_id, view_type)
+ if not res and self._context.get('active_id') and self._context.get('active_model') == 'stock.location':
+ res = '%s%s' % (_('Products: '), self.env['stock.location'].browse(self._context['active_id']).name)
return res
- _columns = {
- 'reception_count': fields.function(_stock_move_count, string="Receipt", type='integer', multi='pickings'),
- 'delivery_count': fields.function(_stock_move_count, string="Delivery", type='integer', multi='pickings'),
- 'qty_available': fields.function(_product_available, multi='qty_available',
- type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
- string='Quantity On Hand',
- fnct_search=_search_product_quantity,
- help="Current quantity of products.\n"
- "In a context with a single Stock Location, this includes "
- "goods stored at this Location, or any of its children.\n"
- "In a context with a single Warehouse, this includes "
- "goods stored in the Stock Location of this Warehouse, or any "
- "of its children.\n"
- "stored in the Stock Location of the Warehouse of this Shop, "
- "or any of its children.\n"
- "Otherwise, this includes goods stored in any Stock Location "
- "with 'internal' type."),
- 'virtual_available': fields.function(_product_available, multi='qty_available',
- type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
- string='Forecast Quantity',
- fnct_search=_search_product_quantity,
- help="Forecast quantity (computed as Quantity On Hand "
- "- Outgoing + Incoming)\n"
- "In a context with a single Stock Location, this includes "
- "goods stored in this location, or any of its children.\n"
- "In a context with a single Warehouse, this includes "
- "goods stored in the Stock Location of this Warehouse, or any "
- "of its children.\n"
- "Otherwise, this includes goods stored in any Stock Location "
- "with 'internal' type."),
- 'incoming_qty': fields.function(_product_available, multi='qty_available',
- type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
- string='Incoming',
- fnct_search=_search_product_quantity,
- help="Quantity of products that are planned to arrive.\n"
- "In a context with a single Stock Location, this includes "
- "goods arriving to this Location, or any of its children.\n"
- "In a context with a single Warehouse, this includes "
- "goods arriving to the Stock Location of this Warehouse, or "
- "any of its children.\n"
- "Otherwise, this includes goods arriving to any Stock "
- "Location with 'internal' type."),
- 'outgoing_qty': fields.function(_product_available, multi='qty_available',
- type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
- string='Outgoing',
- fnct_search=_search_product_quantity,
- help="Quantity of products that are planned to leave.\n"
- "In a context with a single Stock Location, this includes "
- "goods leaving this Location, or any of its children.\n"
- "In a context with a single Warehouse, this includes "
- "goods leaving the Stock Location of this Warehouse, or "
- "any of its children.\n"
- "Otherwise, this includes goods leaving any Stock "
- "Location with 'internal' type."),
- 'orderpoint_ids': fields.one2many('stock.warehouse.orderpoint', 'product_id', 'Minimum Stock Rules'),
- 'nbr_reordering_rules': fields.function(_compute_nbr_reordering_rules, string='Reordering Rules', type='integer', multi=True),
- 'reordering_min_qty': fields.function(_compute_nbr_reordering_rules, type='float', multi=True),
- 'reordering_max_qty': fields.function(_compute_nbr_reordering_rules, type='float', multi=True),
- }
-
- def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
- res = super(product_product, self).fields_view_get(
- cr, uid, view_id=view_id, view_type=view_type, context=context,
- toolbar=toolbar, submenu=submenu)
- if context is None:
- context = {}
- if context.get('location') and isinstance(context['location'], int):
- location_info = self.pool.get('stock.location').browse(cr, uid, context['location'])
- fields=res.get('fields',{})
+ @api.model
+ def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False):
+ res = super(Product, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu)
+ if self._context.get('location') and isinstance(self._context['location'], (int, long)):
+ location = self.env['stock.location'].browse(self._context['location'])
+ fields = res.get('fields')
if fields:
- if location_info.usage == 'supplier':
+ if location.usage == 'supplier':
if fields.get('virtual_available'):
res['fields']['virtual_available']['string'] = _('Future Receipts')
if fields.get('qty_available'):
res['fields']['qty_available']['string'] = _('Received Qty')
-
- if location_info.usage == 'internal':
+ elif location.usage == 'internal':
if fields.get('virtual_available'):
res['fields']['virtual_available']['string'] = _('Forecasted Quantity')
-
- if location_info.usage == 'customer':
+ elif location.usage == 'customer':
if fields.get('virtual_available'):
res['fields']['virtual_available']['string'] = _('Future Deliveries')
if fields.get('qty_available'):
res['fields']['qty_available']['string'] = _('Delivered Qty')
-
- if location_info.usage == 'inventory':
+ elif location.usage == 'inventory':
if fields.get('virtual_available'):
res['fields']['virtual_available']['string'] = _('Future P&L')
if fields.get('qty_available'):
res['fields']['qty_available']['string'] = _('P&L Qty')
-
- if location_info.usage == 'procurement':
+ elif location.usage == 'procurement':
if fields.get('virtual_available'):
res['fields']['virtual_available']['string'] = _('Future Qty')
if fields.get('qty_available'):
res['fields']['qty_available']['string'] = _('Unplanned Qty')
-
- if location_info.usage == 'production':
+ elif location.usage == 'production':
if fields.get('virtual_available'):
res['fields']['virtual_available']['string'] = _('Future Productions')
if fields.get('qty_available'):
res['fields']['qty_available']['string'] = _('Produced Qty')
return res
-
- def action_view_routes(self, cr, uid, ids, context=None):
- template_obj = self.pool.get("product.template")
- templ_ids = list(set([x.product_tmpl_id.id for x in self.browse(cr, uid, ids, context=context)]))
- return template_obj.action_view_routes(cr, uid, templ_ids, context=context)
-
- def onchange_tracking(self, cr, uid, ids, tracking, context=None):
- if not tracking or tracking == 'none':
- return {}
- unassigned_quants = self.pool['stock.quant'].search_count(cr, uid, [('product_id','in', ids), ('lot_id','=', False), ('location_id.usage','=', 'internal')], context=context)
- if unassigned_quants:
- return {'warning' : {
- 'title': _('Warning!'),
- 'message' : _("You have products in stock that have no lot number. You can assign serial numbers by doing an inventory. ")
- }}
- return {}
+ @api.multi
+ def action_view_routes(self):
+ return self.mapped('product_tmpl_id').action_view_routes()
-class product_template(osv.osv):
- _name = 'product.template'
+class ProductTemplate(models.Model):
_inherit = 'product.template'
-
- def _product_available(self, cr, uid, ids, name, arg, context=None):
- prod_available = {}
- product_ids = self.browse(cr, uid, ids, context=context)
- var_ids = []
- for product in product_ids:
- var_ids += [p.id for p in product.product_variant_ids]
- variant_available= self.pool['product.product']._product_available(cr, uid, var_ids, context=context)
- for product in product_ids:
+ property_stock_procurement = fields.Many2one(
+ 'stock.location', "Procurement Location",
+ company_dependent=True, domain=[('usage', 'like', 'procurement')],
+ help="This stock location will be used, instead of the default one, as the source location for stock moves generated by procurements.")
+ property_stock_production = fields.Many2one(
+ 'stock.location', "Production Location",
+ company_dependent=True, domain=[('usage', 'like', 'production')],
+ help="This stock location will be used, instead of the default one, as the source location for stock moves generated by manufacturing orders.")
+ property_stock_inventory = fields.Many2one(
+ 'stock.location', "Inventory Location",
+ company_dependent=True, domain=[('usage', 'like', 'inventory')],
+ 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.")
+ sale_delay = fields.Float(
+ 'Customer Lead Time', default=0,
+ 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.")
+ tracking = fields.Selection([
+ ('serial', 'By Unique Serial Number'),
+ ('lot', 'By Lots'),
+ ('none', 'No Tracking')], string="Tracking", default='none', required=True)
+ description_picking = fields.Text('Description on Picking', translate=True)
+ qty_available = fields.Float(
+ 'Quantity On Hand', compute='_compute_quantities', search='_search_quantities',
+ digits_compute=dp.get_precision('Product Unit of Measure'))
+ virtual_available = fields.Float(
+ 'Forecasted Quantity', compute='_compute_quantities', search='_search_quantities',
+ digits_compute=dp.get_precision('Product Unit of Measure'))
+ incoming_qty = fields.Float(
+ 'Incoming', compute='_compute_quantities', search='_search_quantities',
+ digits_compute=dp.get_precision('Product Unit of Measure'))
+ outgoing_qty = fields.Float(
+ 'Outgoing', compute='_compute_quantities', search='_search_quantities',
+ digits_compute=dp.get_precision('Product Unit of Measure'))
+ location_id = fields.Many2one('stock.location', 'Location')
+ warehouse_id = fields.Many2one('stock.warehouse', 'Warehouse')
+ route_ids = fields.Many2many(
+ 'stock.location.route', 'stock_route_product', 'product_id', 'route_id', 'Routes',
+ domain="[('product_selectable', '=', True)]",
+ 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,...")
+ nbr_reordering_rules = fields.Integer('Reordering Rules', compute='_compute_nbr_reordering_rules')
+ # TDE FIXME: really used ?
+ reordering_min_qty = fields.Float(compute='_compute_nbr_reordering_rules')
+ reordering_max_qty = fields.Float(compute='_compute_nbr_reordering_rules')
+ # TDE FIXME: seems only visible in a view - remove me ?
+ route_from_categ_ids = fields.Many2many(
+ relation="stock.location.route", string="Category Routes",
+ related='categ_id.total_route_ids')
+
+ def _compute_quantities(self):
+ res = self._compute_quantities_dict()
+ for template in self:
+ template.qty_available = res[template.id]['qty_available']
+ template.virtual_available = res[template.id]['virtual_available']
+ template.incoming_qty = res[template.id]['incoming_qty']
+ template.outgoing_qty = res[template.id]['outgoing_qty']
+
+ def _product_available(self, name, arg):
+ return self._compute_quantities_dict()
+
+ def _compute_quantities_dict(self):
+ # TDE FIXME: why not using directly the function fields ?
+ variants_available = self.mapped('product_variant_ids')._product_available()
+ prod_available = {}
+ for template in self:
qty_available = 0
virtual_available = 0
incoming_qty = 0
outgoing_qty = 0
- for p in product.product_variant_ids:
- qty_available += variant_available[p.id]["qty_available"]
- virtual_available += variant_available[p.id]["virtual_available"]
- incoming_qty += variant_available[p.id]["incoming_qty"]
- outgoing_qty += variant_available[p.id]["outgoing_qty"]
- prod_available[product.id] = {
+ for p in template.product_variant_ids:
+ qty_available += variants_available[p.id]["qty_available"]
+ virtual_available += variants_available[p.id]["virtual_available"]
+ incoming_qty += variants_available[p.id]["incoming_qty"]
+ outgoing_qty += variants_available[p.id]["outgoing_qty"]
+ prod_available[template.id] = {
"qty_available": qty_available,
"virtual_available": virtual_available,
"incoming_qty": incoming_qty,
@@ -372,164 +403,101 @@ class product_template(osv.osv):
}
return prod_available
- def _search_product_quantity(self, cr, uid, obj, name, domain, context):
- prod = self.pool.get("product.product")
- product_variant_ids = prod.search(cr, uid, domain, context=context)
- return [('product_variant_ids', 'in', product_variant_ids)]
+ def _search_quantities(self, operator, value):
+ # TDE FIXME: does this work anyway ?
+ domain = [] # TDE ADDED
+ product_variant_ids = self.env['product.product'].search(domain)
+ return [('product_variant_ids', 'in', product_variant_ids.ids)]
- def _compute_nbr_reordering_rules(self, cr, uid, ids, field_names=None, arg=None, context=None):
- res = {k: {'nbr_reordering_rules': 0, 'reordering_min_qty': 0, 'reordering_max_qty': 0} for k in ids}
- product_data = self.pool['stock.warehouse.orderpoint'].read_group(cr, uid, [('product_id.product_tmpl_id', 'in', ids)], ['product_id', 'product_min_qty', 'product_max_qty'], ['product_id'], context=context)
+ def _compute_nbr_reordering_rules(self):
+ res = {k: {'nbr_reordering_rules': 0, 'reordering_min_qty': 0, 'reordering_max_qty': 0} for k in self.ids}
+ product_data = self.env['stock.warehouse.orderpoint'].read_group([('product_id.product_tmpl_id', 'in', self.ids)], ['product_id', 'product_min_qty', 'product_max_qty'], ['product_id'])
for data in product_data:
- p = self.pool['product.product'].browse(cr, uid, [data['product_id'][0]], context=context)
- product_tmpl_id = p.product_tmpl_id.id
+ product = self.env['product.product'].browse([data['product_id'][0]])
+ product_tmpl_id = product.product_tmpl_id.id
res[product_tmpl_id]['nbr_reordering_rules'] += int(data['product_id_count'])
res[product_tmpl_id]['reordering_min_qty'] = data['product_min_qty']
res[product_tmpl_id]['reordering_max_qty'] = data['product_max_qty']
- return res
+ for template in self:
+ self.nbr_reordering_rules = res[template.id]['nbr_reordering_rules']
+ self.reordering_min_qty = res[template.id]['reordering_min_qty']
+ self.reordering_max_qty = res[template.id]['reordering_max_qty']
- def _get_product_template_type(self, cr, uid, context=None):
- res = super(product_template, self)._get_product_template_type(cr, uid, context=context)
+ @api.model
+ def _get_product_template_type(self):
+ res = super(ProductTemplate, self)._get_product_template_type()
if 'product' not in [item[0] for item in res]:
res.append(('product', _('Stockable Product')))
return res
- _columns = {
- 'property_stock_procurement': fields.property(
- type='many2one',
- relation='stock.location',
- string="Procurement Location",
- domain=[('usage','like','procurement')],
- help="This stock location will be used, instead of the default one, as the source location for stock moves generated by procurements."),
- 'property_stock_production': fields.property(
- type='many2one',
- relation='stock.location',
- string="Production Location",
- domain=[('usage','like','production')],
- help="This stock location will be used, instead of the default one, as the source location for stock moves generated by manufacturing orders."),
- 'property_stock_inventory': fields.property(
- type='many2one',
- relation='stock.location',
- string="Inventory Location",
- domain=[('usage','like','inventory')],
- 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."),
- '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."),
- 'tracking': fields.selection(selection=[('serial', 'By Unique Serial Number'), ('lot', 'By Lots'), ('none', 'No Tracking')], string="Tracking", required=True),
- 'description_picking': fields.text('Description on Picking', translate=True),
- # sum of product variant qty
- # 'reception_count': fields.function(_product_available, multi='qty_available',
- # fnct_search=_search_product_quantity, type='float', string='Quantity On Hand'),
- # 'delivery_count': fields.function(_product_available, multi='qty_available',
- # fnct_search=_search_product_quantity, type='float', string='Quantity On Hand'),
- 'qty_available': fields.function(_product_available, multi='qty_available', digits_compute=dp.get_precision('Product Unit of Measure'),
- fnct_search=_search_product_quantity, type='float', string='Quantity On Hand'),
- 'virtual_available': fields.function(_product_available, multi='qty_available', digits_compute=dp.get_precision('Product Unit of Measure'),
- fnct_search=_search_product_quantity, type='float', string='Forecasted Quantity'),
- 'incoming_qty': fields.function(_product_available, multi='qty_available', digits_compute=dp.get_precision('Product Unit of Measure'),
- fnct_search=_search_product_quantity, type='float', string='Incoming'),
- 'outgoing_qty': fields.function(_product_available, multi='qty_available', digits_compute=dp.get_precision('Product Unit of Measure'),
- fnct_search=_search_product_quantity, type='float', string='Outgoing'),
- 'location_id': fields.dummy(string='Location', relation='stock.location', type='many2one'),
- 'warehouse_id': fields.dummy(string='Warehouse', relation='stock.warehouse', type='many2one'),
- 'route_ids': fields.many2many('stock.location.route', 'stock_route_product', 'product_id', 'route_id', 'Routes', domain="[('product_selectable', '=', True)]",
- 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,..."),
- 'nbr_reordering_rules': fields.function(_compute_nbr_reordering_rules, string='Reordering Rules', type='integer', multi=True),
- 'reordering_min_qty': fields.function(_compute_nbr_reordering_rules, type='float', multi=True),
- 'reordering_max_qty': fields.function(_compute_nbr_reordering_rules, type='float', multi=True),
- 'route_from_categ_ids': fields.related('categ_id', 'total_route_ids', type="many2many", relation="stock.location.route", string="Category Routes"),
- }
+ @api.onchange('tracking')
+ def onchange_tracking(self):
+ return self.mapped('product_variant_ids').onchange_tracking()
- _defaults = {
- 'sale_delay': 0,
- 'tracking': 'none',
- }
-
- def action_view_routes(self, cr, uid, ids, context=None):
- route_obj = self.pool.get("stock.location.route")
- act_obj = self.pool.get('ir.actions.act_window')
- mod_obj = self.pool.get('ir.model.data')
- product_route_ids = set()
- for product in self.browse(cr, uid, ids, context=context):
- product_route_ids |= set([r.id for r in product.route_ids])
- product_route_ids |= set([r.id for r in product.categ_id.total_route_ids])
- route_ids = route_obj.search(cr, uid, ['|', ('id', 'in', list(product_route_ids)), ('warehouse_selectable', '=', True)], context=context)
- result = mod_obj.xmlid_to_res_id(cr, uid, 'stock.action_routes_form', raise_if_not_found=True)
- result = act_obj.read(cr, uid, [result], context=context)[0]
- result['domain'] = "[('id','in',[" + ','.join(map(str, route_ids)) + "])]"
- return result
-
- def onchange_tracking(self, cr, uid, ids, tracking, context=None):
- if not tracking:
- return {}
- product_product = self.pool['product.product']
- variant_ids = product_product.search(cr, uid, [('product_tmpl_id', 'in', ids)], context=context)
- return product_product.onchange_tracking(cr, uid, variant_ids, tracking, context=context)
-
- def _get_products(self, cr, uid, ids, context=None):
- products = []
- for prodtmpl in self.browse(cr, uid, ids, context=None):
- products += [x.id for x in prodtmpl.product_variant_ids]
- return products
-
- def _get_act_window_dict(self, cr, uid, name, context=None):
- mod_obj = self.pool.get('ir.model.data')
- act_obj = self.pool.get('ir.actions.act_window')
- result = mod_obj.xmlid_to_res_id(cr, uid, name, raise_if_not_found=True)
- result = act_obj.read(cr, uid, [result], context=context)[0]
- return result
-
- def action_open_quants(self, cr, uid, ids, context=None):
- products = self._get_products(cr, uid, ids, context=context)
- result = self._get_act_window_dict(cr, uid, 'stock.product_open_quants', context=context)
- result['domain'] = "[('product_id','in',[" + ','.join(map(str, products)) + "])]"
- result['context'] = "{'search_default_locationgroup': 1, 'search_default_internal_loc': 1}"
- return result
-
- def action_view_orderpoints(self, cr, uid, ids, context=None):
- products = self._get_products(cr, uid, ids, context=context)
- result = self._get_act_window_dict(cr, uid, 'stock.product_open_orderpoint', context=context)
- if len(ids) == 1 and len(products) == 1:
- result['context'] = "{'default_product_id': " + str(products[0]) + ", 'search_default_product_id': " + str(products[0]) + "}"
- else:
- result['domain'] = "[('product_id','in',[" + ','.join(map(str, products)) + "])]"
- result['context'] = "{}"
- return result
-
- def action_view_stock_moves(self, cr, uid, ids, context=None):
- products = self._get_products(cr, uid, ids, context=context)
- result = self._get_act_window_dict(cr, uid, 'stock.act_product_stock_move_open', context=context)
- if products:
- result['context'] = "{'default_product_id': %d}" % products[0]
- result['domain'] = "[('product_id.product_tmpl_id','in',[" + ','.join(map(str,ids)) + "])]"
- return result
-
- def write(self, cr, uid, ids, vals, context=None):
+ @api.multi
+ def write(self, vals):
if 'uom_id' in vals:
- new_uom = self.pool.get('product.uom').browse(cr, uid, vals['uom_id'], context=context)
- for product in self.browse(cr, uid, ids, context=context):
- old_uom = product.uom_id
- if old_uom != new_uom:
- if self.pool.get('stock.move').search(cr, uid, [('product_id', 'in', [x.id for x in product.product_variant_ids]), ('state', '=', 'done')], limit=1, context=context):
- raise UserError(_("You can not change the unit of measure of a product that has already been used in a done stock move. If you need to change the unit of measure, you may deactivate this product."))
- return super(product_template, self).write(cr, uid, ids, vals, context=context)
+ new_uom = self.env['product.uom'].browse(vals['uom_id'])
+ updated = self.filtered(lambda template: template.uom_id != new_uom)
+ done_moves = self.env['stock.move'].search([('product_id', 'in', updated.mapped('product_variant_ids').ids)], limit=1)
+ if done_moves:
+ raise UserError(_("You can not change the unit of measure of a product that has already been used in a done stock move. If you need to change the unit of measure, you may deactivate this product."))
+ return super(ProductTemplate, self).write(vals)
+
+ @api.multi
+ def action_view_routes(self):
+ routes = self.mapped('route_ids') | self.mapped('categ_id').mapped('total_route_ids') | self.env['stock.location.route'].search([('warehouse_selectable', '=', True)])
+ action = self.env.ref('stock.action_routes_form').read()[0]
+ action['domain'] = [('id', 'in', routes.ids)]
+ return action
+
+ @api.multi
+ def action_open_quants(self):
+ products = self.mapped('product_variant_ids')
+ action = self.env.ref('stock.product_open_quants').read()[0]
+ action['domain'] = [('product_id', 'in', products.ids)]
+ action['context'] = {'search_default_locationgroup': 1, 'search_default_internal_loc': 1}
+ return action
+
+ @api.multi
+ def action_view_orderpoints(self):
+ products = self.mapped('product_variant_ids')
+ action = self.env.ref('stock.product_open_orderpoint').read()[0]
+ if products and len(products) == 1:
+ action['context'] = {'default_product_id': products.ids[0], 'search_default_product_id': products.ids[0]}
+ else:
+ action['domain'] = [('product_id', 'in', products.ids)]
+ action['context'] = {}
+ return action
+
+ @api.multi
+ def action_view_stock_moves(self):
+ products = self.mapped('product_variant_ids')
+ action = self.env.ref('stock.act_product_stock_move_open').read()[0]
+ if self:
+ action['context'] = {'default_product_id': products.ids[0]}
+ action['domain'] = [('product_id.product_tmpl_id', 'in', products.ids)]
+ return action
-class product_category(osv.osv):
+class ProductCategory(models.Model):
_inherit = 'product.category'
- def calculate_total_routes(self, cr, uid, ids, name, args, context=None):
- res = {}
- for categ in self.browse(cr, uid, ids, context=context):
- categ2 = categ
- routes = [x.id for x in categ.route_ids]
- while categ2.parent_id:
- categ2 = categ2.parent_id
- routes += [x.id for x in categ2.route_ids]
- res[categ.id] = routes
- return res
+ route_ids = fields.Many2many(
+ 'stock.location.route', 'stock_location_route_categ', 'categ_id', 'route_id', 'Routes',
+ domain="[('product_categ_selectable', '=', True)]")
+ 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")
+ total_route_ids = fields.Many2many(
+ 'stock.location.route', string='Total routes', compute='_compute_total_route_ids',
+ readonly=True)
- _columns = {
- 'route_ids': fields.many2many('stock.location.route', 'stock_location_route_categ', 'categ_id', 'route_id', 'Routes', domain="[('product_categ_selectable', '=', True)]"),
- '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"),
- 'total_route_ids': fields.function(calculate_total_routes, relation='stock.location.route', type='many2many', string='Total routes', readonly=True),
- }
+ @api.one
+ def _compute_total_route_ids(self):
+ category = self
+ routes = self.route_ids
+ while category.parent_id:
+ category = category.parent_id
+ routes |= category.route_ids
+ self.total_route_ids = routes
diff --git a/addons/stock/models/product_strategy.py b/addons/stock/models/product_strategy.py
index 3b8d19e4d40..f4987babd43 100644
--- a/addons/stock/models/product_strategy.py
+++ b/addons/stock/models/product_strategy.py
@@ -1,53 +1,50 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from openerp.osv import fields, osv
+from odoo import fields, models
-class product_removal_strategy(osv.osv):
+class RemovalStrategy(models.Model):
_name = 'product.removal'
_description = 'Removal Strategy'
- _columns = {
- 'name': fields.char('Name', required=True),
- 'method': fields.char("Method", required=True, help="FIFO, LIFO..."),
- }
+ name = fields.Char('Name', required=True)
+ method = fields.Char("Method", required=True, help="FIFO, LIFO...")
-class product_putaway_strategy(osv.osv):
+class PutAwayStrategy(models.Model):
_name = 'product.putaway'
_description = 'Put Away Strategy'
- def _get_putaway_options(self, cr, uid, context=None):
+ name = fields.Char('Name', required=True)
+ method = fields.Selection('_get_putaway_options', "Method", default='fixed', required=True)
+ fixed_location_ids = fields.One2many(
+ 'stock.fixed.putaway.strat', 'putaway_id', 'Fixed Locations Per Product Category', copy=True,
+ help="When the method is fixed, this location will be used to store the products")
+
+ def _get_putaway_options(self):
return [('fixed', 'Fixed Location')]
- _columns = {
- 'name': fields.char('Name', required=True),
- 'method': fields.selection(_get_putaway_options, "Method", required=True),
- '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", copy=True),
- }
+ def _putaway_apply_fixed(self, product):
+ for strat in self.fixed_location_ids:
+ categ = product.categ_id
+ while categ:
+ if strat.category_id.id == categ.id:
+ return strat.fixed_location_id.id
+ categ = categ.parent_id
+ return self.env['stock.location']
- _defaults = {
- 'method': 'fixed',
- }
-
- def putaway_apply(self, cr, uid, ids, product, context=None):
- putaway_strat = self.browse(cr, uid, ids[0], context=context)
- if putaway_strat.method == 'fixed':
- for strat in putaway_strat.fixed_location_ids:
- categ = product.categ_id
- while categ:
- if strat.category_id.id == categ.id:
- return strat.fixed_location_id.id
- categ = categ.parent_id
+ def putaway_apply(self, product):
+ if hasattr(self, '_putaway_apply_%s' % (self.method)):
+ return getattr(self, '_putaway_apply_%s' % (self.method))
+ return self.env['stock.location']
-class fixed_putaway_strat(osv.osv):
+class FixedPutAwayStrategy(models.Model):
_name = 'stock.fixed.putaway.strat'
_order = 'sequence'
- _columns = {
- 'putaway_id': fields.many2one('product.putaway', 'Put Away Method', required=True),
- 'category_id': fields.many2one('product.category', 'Product Category', required=True),
- 'fixed_location_id': fields.many2one('stock.location', 'Location', required=True),
- 'sequence': fields.integer('Priority', help="Give to the more specialized category, a higher priority to have them in top of the list."),
- }
\ No newline at end of file
+
+ putaway_id = fields.Many2one('product.putaway', 'Put Away Method', required=True)
+ category_id = fields.Many2one('product.category', 'Product Category', required=True)
+ fixed_location_id = fields.Many2one('stock.location', 'Location', required=True)
+ sequence = fields.Integer('Priority', help="Give to the more specialized category, a higher priority to have them in top of the list.")
diff --git a/addons/stock/views/product_views.xml b/addons/stock/views/product_views.xml
index fc668c0d2e6..8641f9e0909 100644
--- a/addons/stock/views/product_views.xml
+++ b/addons/stock/views/product_views.xml
@@ -124,7 +124,7 @@
-
+