[MIGR] stock: product and product strategy

Product models are migrated to new API. No change is performed to code
structure or functional structure. Some code cleaning may be performed when
migrating to simplify the code. Other files may be updated due to the move
towards the new API.

Impacted models

 - product.product
 - product.template
 - product.removal
 - product.putaway
 - stock.fixed.putaway.strat
This commit is contained in:
Thibault Delavallée
2016-04-29 12:18:46 +02:00
parent d00a054a38
commit b9853db8b5
3 changed files with 417 additions and 452 deletions
+386 -418
View File
@@ -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
+30 -33
View File
@@ -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."),
}
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.")
+1 -1
View File
@@ -124,7 +124,7 @@
<field name="inherit_id" ref="product.product_template_form_view"/>
<field name="arch" type="xml">
<div name="weight" position="after">
<field name="tracking" on_change="onchange_tracking(tracking)" widget="radio" attrs="{'invisible':[('type', 'in', ['service', 'digital'])]}" groups="stock.group_production_lot"/>
<field name="tracking" widget="radio" attrs="{'invisible':[('type', 'in', ['service', 'digital'])]}" groups="stock.group_production_lot"/>
</div>
<field name="company_id" position="after">
<field name="route_ids" widget="many2many_checkboxes" attrs="{'invisible':[('type', 'in', ['service', 'digital'])]}"/>