[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:
+386
-418
@@ -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
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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'])]}"/>
|
||||
|
||||
Reference in New Issue
Block a user