[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:
+384
-416
@@ -1,96 +1,173 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
from openerp.osv import fields, osv
|
from odoo import api, fields, models, _
|
||||||
from openerp.tools.translate import _
|
from odoo.addons import decimal_precision as dp
|
||||||
from openerp.tools.safe_eval import safe_eval as eval
|
from odoo.exceptions import UserError
|
||||||
import openerp.addons.decimal_precision as dp
|
from odoo.tools.safe_eval import safe_eval as eval
|
||||||
from openerp.tools.float_utils import float_round
|
from odoo.tools.float_utils import float_round
|
||||||
from openerp.exceptions import UserError
|
|
||||||
|
|
||||||
|
|
||||||
class product_product(osv.osv):
|
class Product(models.Model):
|
||||||
_inherit = "product.product"
|
_inherit = "product.product"
|
||||||
|
|
||||||
def _stock_move_count(self, cr, uid, ids, field_name, arg, context=None):
|
reception_count = fields.Integer('Receipt', compute='_compute_reception_count')
|
||||||
res = dict([(id, {'reception_count': 0, 'delivery_count': 0}) for id in ids])
|
delivery_count = fields.Integer('Delivery', compute='_compute_delivery_count')
|
||||||
move_pool=self.pool.get('stock.move')
|
stock_quant_ids = fields.One2many('stock.quant', 'product_id', help='Technical: used to compute quantities.')
|
||||||
moves = move_pool.read_group(cr, uid, [
|
stock_move_ids = fields.One2many('stock.move', 'product_id', help='Technical: used to compute quantities.')
|
||||||
('product_id', 'in', ids),
|
qty_available = fields.Float(
|
||||||
('location_id.usage', '!=', 'internal'),
|
'Quantity On Hand', compute='_compute_quantities', search='_search_qty_available',
|
||||||
('location_dest_id.usage', '=', 'internal'),
|
digits_compute=dp.get_precision('Product Unit of Measure'),
|
||||||
('state','in',('confirmed','assigned','pending'))
|
help="Current quantity of products.\n"
|
||||||
], ['product_id'], ['product_id'])
|
"In a context with a single Stock Location, this includes "
|
||||||
for move in moves:
|
"goods stored at this Location, or any of its children.\n"
|
||||||
product_id = move['product_id'][0]
|
"In a context with a single Warehouse, this includes "
|
||||||
res[product_id]['reception_count'] = move['product_id_count']
|
"goods stored in the Stock Location of this Warehouse, or any "
|
||||||
moves = move_pool.read_group(cr, uid, [
|
"of its children.\n"
|
||||||
('product_id', 'in', ids),
|
"stored in the Stock Location of the Warehouse of this Shop, "
|
||||||
('location_id.usage', '=', 'internal'),
|
"or any of its children.\n"
|
||||||
('location_dest_id.usage', '!=', 'internal'),
|
"Otherwise, this includes goods stored in any Stock Location "
|
||||||
('state','in',('confirmed','assigned','pending'))
|
"with 'internal' type.")
|
||||||
], ['product_id'], ['product_id'])
|
virtual_available = fields.Float(
|
||||||
for move in moves:
|
'Forecast Quantity', compute='_compute_quantities', search='_search_virtual_available',
|
||||||
product_id = move['product_id'][0]
|
digits_compute=dp.get_precision('Product Unit of Measure'),
|
||||||
res[product_id]['delivery_count'] = move['product_id_count']
|
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
|
return res
|
||||||
|
|
||||||
def view_header_get(self, cr, user, view_id, view_type, context=None):
|
def _get_domain_locations(self):
|
||||||
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):
|
|
||||||
'''
|
'''
|
||||||
Parses the context and returns a list of location_ids based on it.
|
Parses the context and returns a list of location_ids based on it.
|
||||||
It will return all stock locations when no parameters are given
|
It will return all stock locations when no parameters are given
|
||||||
Possible parameters are shop, warehouse, location, force_company, compute_child
|
Possible parameters are shop, warehouse, location, force_company, compute_child
|
||||||
'''
|
'''
|
||||||
context = context or {}
|
# TDE FIXME: clean that brol, context seems overused
|
||||||
|
Warehouse = self.env['stock.warehouse']
|
||||||
location_obj = self.pool.get('stock.location')
|
|
||||||
warehouse_obj = self.pool.get('stock.warehouse')
|
|
||||||
|
|
||||||
location_ids = []
|
location_ids = []
|
||||||
if context.get('location', False):
|
if self.env.context.get('location', False):
|
||||||
if isinstance(context['location'], (int, long)):
|
if isinstance(self.env.context['location'], (int, long)):
|
||||||
location_ids = [context['location']]
|
location_ids = [self.env.context['location']]
|
||||||
elif isinstance(context['location'], basestring):
|
elif isinstance(self.env.context['location'], basestring):
|
||||||
domain = [('complete_name','ilike',context['location'])]
|
domain = [('complete_name', 'ilike', self.env.context['location'])]
|
||||||
if context.get('force_company', False):
|
if self.env.context.get('force_company', False):
|
||||||
domain += [('company_id', '=', context['force_company'])]
|
domain += [('company_id', '=', self.env.context['force_company'])]
|
||||||
location_ids = location_obj.search(cr, uid, domain, context=context)
|
location_ids = self.env['stock.location'].search(domain).ids
|
||||||
else:
|
else:
|
||||||
location_ids = context['location']
|
location_ids = self.env.context['location']
|
||||||
else:
|
else:
|
||||||
if context.get('warehouse', False):
|
if self.env.context.get('warehouse', False):
|
||||||
if isinstance(context['warehouse'], (int, long)):
|
if isinstance(self.env.context['warehouse'], (int, long)):
|
||||||
wids = [context['warehouse']]
|
wids = [self.env.context['warehouse']]
|
||||||
elif isinstance(context['warehouse'], basestring):
|
elif isinstance(self.env.context['warehouse'], basestring):
|
||||||
domain = [('name', 'ilike', context['warehouse'])]
|
domain = [('name', 'ilike', self.env.context['warehouse'])]
|
||||||
if context.get('force_company', False):
|
if self.env.context.get('force_company', False):
|
||||||
domain += [('company_id', '=', context['force_company'])]
|
domain += [('company_id', '=', self.env.context['force_company'])]
|
||||||
wids = warehouse_obj.search(cr, uid, domain, context=context)
|
wids = Warehouse.search(domain).ids
|
||||||
else:
|
else:
|
||||||
wids = context['warehouse']
|
wids = self.env.context['warehouse']
|
||||||
else:
|
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)
|
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'
|
def _get_domain_locations_new(self, location_ids, company_id=False, compute_child=True):
|
||||||
domain = context.get('force_company', False) and ['&', ('company_id', '=', context['force_company'])] or []
|
operator = compute_child and 'child_of' or 'in'
|
||||||
locations = location_obj.browse(cr, uid, location_ids, context=context)
|
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
|
# 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
|
# The code has been modified because having one location with parent_left being
|
||||||
# 0 make the whole domain unusable
|
# 0 make the whole domain unusable
|
||||||
hierarchical_locations = [location for location in locations if location.parent_left != 0 and operator == "child_of"]
|
hierarchical_locations = locations.filtered(lambda location: location.parent_left != 0 and operator == "child_of")
|
||||||
other_locations = [location for location in locations if location not in hierarchical_locations]
|
other_locations = locations.filtered(lambda location: location not in hierarchical_locations) # TDE: set - set ?
|
||||||
loc_domain = []
|
loc_domain = []
|
||||||
dest_loc_domain = []
|
dest_loc_domain = []
|
||||||
for location in hierarchical_locations:
|
for location in hierarchical_locations:
|
||||||
@@ -113,258 +190,212 @@ class product_product(osv.osv):
|
|||||||
domain + ['&'] + loc_domain + ['!'] + dest_loc_domain
|
domain + ['&'] + loc_domain + ['!'] + dest_loc_domain
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_domain_dates(self, cr, uid, ids, context):
|
def _search_virtual_available(self, operator, value):
|
||||||
from_date = context.get('from_date', False)
|
# TDE FIXME: should probably clean the search methods
|
||||||
to_date = context.get('to_date', False)
|
return self._search_product_quantity(operator, value, 'virtual_available')
|
||||||
domain = []
|
|
||||||
if from_date:
|
|
||||||
domain.append(('date', '>=', from_date))
|
|
||||||
if to_date:
|
|
||||||
domain.append(('date', '<=', to_date))
|
|
||||||
return domain
|
|
||||||
|
|
||||||
def _product_available(self, cr, uid, ids, field_names=None, arg=False, context=None):
|
def _search_incoming_qty(self, operator, value):
|
||||||
context = context or {}
|
# TDE FIXME: should probably clean the search methods
|
||||||
field_names = field_names or []
|
return self._search_product_quantity(operator, value, 'incoming_qty')
|
||||||
|
|
||||||
domain_products = [('product_id', 'in', ids)]
|
def _search_outgoing_qty(self, operator, value):
|
||||||
domain_quant, domain_move_in, domain_move_out = [], [], []
|
# TDE FIXME: should probably clean the search methods
|
||||||
domain_quant_loc, domain_move_in_loc, domain_move_out_loc = self._get_domain_locations(cr, uid, ids, context=context)
|
return self._search_product_quantity(operator, value, 'outgoing_qty')
|
||||||
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
|
|
||||||
|
|
||||||
if context.get('lot_id'):
|
def _search_product_quantity(self, operator, value, field):
|
||||||
domain_quant.append(('lot_id', '=', context['lot_id']))
|
# TDE FIXME: should probably clean the search methods
|
||||||
if context.get('owner_id'):
|
# to prevent sql injections
|
||||||
domain_quant.append(('owner_id', '=', context['owner_id']))
|
if field not in ('qty_available', 'virtual_available', 'incoming_qty', 'outgoing_qty'):
|
||||||
owner_domain = ('restrict_partner_id', '=', context['owner_id'])
|
raise UserError('Invalid domain left operand')
|
||||||
domain_move_in.append(owner_domain)
|
if operator not in ('<', '>', '=', '!=', '<=', '>='):
|
||||||
domain_move_out.append(owner_domain)
|
raise UserError('Invalid domain operator')
|
||||||
if context.get('package_id'):
|
if not isinstance(value, (float, int)):
|
||||||
domain_quant.append(('package_id', '=', context['package_id']))
|
raise UserError('Invalid domain right operand')
|
||||||
|
|
||||||
domain_move_in += domain_move_in_loc
|
if operator == '=':
|
||||||
domain_move_out += domain_move_out_loc
|
operator = '=='
|
||||||
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)
|
|
||||||
|
|
||||||
domain_quant += domain_quant_loc
|
# TODO: Still optimization possible when searching virtual quantities
|
||||||
quants = self.pool.get('stock.quant').read_group(cr, uid, domain_quant, ['product_id', 'qty'], ['product_id'], context=context)
|
ids = []
|
||||||
quants = dict(map(lambda x: (x['product_id'][0], x['qty']), quants))
|
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))
|
def _search_qty_available(self, operator, value):
|
||||||
moves_out = dict(map(lambda x: (x['product_id'][0], x['product_qty']), moves_out))
|
# TDE FIXME: should probably clean the search methods
|
||||||
res = {}
|
if value == 0.0 and operator in ('==', '>=', '<='):
|
||||||
ctx = context.copy()
|
return self._search_product_quantity(operator, value, 'qty_available')
|
||||||
ctx.update({'prefetch_fields': False})
|
product_ids = self._search_qty_available_new(operator, value, self._context.get('lot_id'), self._context.get('owner_id'), self._context.get('package_id'))
|
||||||
for product in self.browse(cr, uid, ids, context=ctx):
|
return [('id', 'in', product_ids)]
|
||||||
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_product_quantity(self, cr, uid, obj, name, domain, context):
|
def _search_qty_available_new(self, operator, value, lot_id=False, owner_id=False, package_id=False):
|
||||||
res = []
|
# TDE FIXME: should probably clean the search methods
|
||||||
for field, operator, value in domain:
|
product_ids = set()
|
||||||
#to prevent sql injections
|
domain_quant = self._get_domain_locations()[0]
|
||||||
assert field in ('qty_available', 'virtual_available', 'incoming_qty', 'outgoing_qty'), 'Invalid domain left operand'
|
if lot_id:
|
||||||
assert operator in ('<', '>', '=', '!=', '<=', '>='), 'Invalid domain operator'
|
domain_quant.append(('lot_id', '=', lot_id))
|
||||||
assert isinstance(value, (float, int)), 'Invalid domain right operand'
|
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 == '=':
|
def _compute_nbr_reordering_rules(self):
|
||||||
operator = '=='
|
read_group_res = self.env['stock.warehouse.orderpoint'].read_group(
|
||||||
|
[('product_id', 'in', self.ids)],
|
||||||
ids = []
|
['product_id', 'product_min_qty', 'product_max_qty'],
|
||||||
if name == 'qty_available' and (value != 0.0 or operator not in ('==', '>=', '<=')):
|
['product_id'])
|
||||||
res.append(('id', 'in', self._search_qty_available(cr, uid, operator, value, context)))
|
res = dict.fromkeys(self.ids, dict())
|
||||||
else:
|
for data in read_group_res:
|
||||||
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:
|
|
||||||
res[data['product_id'][0]]['nbr_reordering_rules'] = int(data['product_id_count'])
|
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_min_qty'] = data['product_min_qty']
|
||||||
res[data['product_id'][0]]['reordering_max_qty'] = data['product_max_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
|
return res
|
||||||
|
|
||||||
_columns = {
|
@api.model
|
||||||
'reception_count': fields.function(_stock_move_count, string="Receipt", type='integer', multi='pickings'),
|
def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False):
|
||||||
'delivery_count': fields.function(_stock_move_count, string="Delivery", type='integer', multi='pickings'),
|
res = super(Product, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu)
|
||||||
'qty_available': fields.function(_product_available, multi='qty_available',
|
if self._context.get('location') and isinstance(self._context['location'], (int, long)):
|
||||||
type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
|
location = self.env['stock.location'].browse(self._context['location'])
|
||||||
string='Quantity On Hand',
|
fields = res.get('fields')
|
||||||
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',{})
|
|
||||||
if fields:
|
if fields:
|
||||||
if location_info.usage == 'supplier':
|
if location.usage == 'supplier':
|
||||||
if fields.get('virtual_available'):
|
if fields.get('virtual_available'):
|
||||||
res['fields']['virtual_available']['string'] = _('Future Receipts')
|
res['fields']['virtual_available']['string'] = _('Future Receipts')
|
||||||
if fields.get('qty_available'):
|
if fields.get('qty_available'):
|
||||||
res['fields']['qty_available']['string'] = _('Received Qty')
|
res['fields']['qty_available']['string'] = _('Received Qty')
|
||||||
|
elif location.usage == 'internal':
|
||||||
if location_info.usage == 'internal':
|
|
||||||
if fields.get('virtual_available'):
|
if fields.get('virtual_available'):
|
||||||
res['fields']['virtual_available']['string'] = _('Forecasted Quantity')
|
res['fields']['virtual_available']['string'] = _('Forecasted Quantity')
|
||||||
|
elif location.usage == 'customer':
|
||||||
if location_info.usage == 'customer':
|
|
||||||
if fields.get('virtual_available'):
|
if fields.get('virtual_available'):
|
||||||
res['fields']['virtual_available']['string'] = _('Future Deliveries')
|
res['fields']['virtual_available']['string'] = _('Future Deliveries')
|
||||||
if fields.get('qty_available'):
|
if fields.get('qty_available'):
|
||||||
res['fields']['qty_available']['string'] = _('Delivered Qty')
|
res['fields']['qty_available']['string'] = _('Delivered Qty')
|
||||||
|
elif location.usage == 'inventory':
|
||||||
if location_info.usage == 'inventory':
|
|
||||||
if fields.get('virtual_available'):
|
if fields.get('virtual_available'):
|
||||||
res['fields']['virtual_available']['string'] = _('Future P&L')
|
res['fields']['virtual_available']['string'] = _('Future P&L')
|
||||||
if fields.get('qty_available'):
|
if fields.get('qty_available'):
|
||||||
res['fields']['qty_available']['string'] = _('P&L Qty')
|
res['fields']['qty_available']['string'] = _('P&L Qty')
|
||||||
|
elif location.usage == 'procurement':
|
||||||
if location_info.usage == 'procurement':
|
|
||||||
if fields.get('virtual_available'):
|
if fields.get('virtual_available'):
|
||||||
res['fields']['virtual_available']['string'] = _('Future Qty')
|
res['fields']['virtual_available']['string'] = _('Future Qty')
|
||||||
if fields.get('qty_available'):
|
if fields.get('qty_available'):
|
||||||
res['fields']['qty_available']['string'] = _('Unplanned Qty')
|
res['fields']['qty_available']['string'] = _('Unplanned Qty')
|
||||||
|
elif location.usage == 'production':
|
||||||
if location_info.usage == 'production':
|
|
||||||
if fields.get('virtual_available'):
|
if fields.get('virtual_available'):
|
||||||
res['fields']['virtual_available']['string'] = _('Future Productions')
|
res['fields']['virtual_available']['string'] = _('Future Productions')
|
||||||
if fields.get('qty_available'):
|
if fields.get('qty_available'):
|
||||||
res['fields']['qty_available']['string'] = _('Produced Qty')
|
res['fields']['qty_available']['string'] = _('Produced Qty')
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
@api.multi
|
||||||
def action_view_routes(self, cr, uid, ids, context=None):
|
def action_view_routes(self):
|
||||||
template_obj = self.pool.get("product.template")
|
return self.mapped('product_tmpl_id').action_view_routes()
|
||||||
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 {}
|
|
||||||
|
|
||||||
|
|
||||||
class product_template(osv.osv):
|
class ProductTemplate(models.Model):
|
||||||
_name = 'product.template'
|
|
||||||
_inherit = 'product.template'
|
_inherit = 'product.template'
|
||||||
|
|
||||||
def _product_available(self, cr, uid, ids, name, arg, context=None):
|
property_stock_procurement = fields.Many2one(
|
||||||
prod_available = {}
|
'stock.location', "Procurement Location",
|
||||||
product_ids = self.browse(cr, uid, ids, context=context)
|
company_dependent=True, domain=[('usage', 'like', 'procurement')],
|
||||||
var_ids = []
|
help="This stock location will be used, instead of the default one, as the source location for stock moves generated by procurements.")
|
||||||
for product in product_ids:
|
property_stock_production = fields.Many2one(
|
||||||
var_ids += [p.id for p in product.product_variant_ids]
|
'stock.location', "Production Location",
|
||||||
variant_available= self.pool['product.product']._product_available(cr, uid, var_ids, context=context)
|
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')
|
||||||
|
|
||||||
for product in product_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
|
qty_available = 0
|
||||||
virtual_available = 0
|
virtual_available = 0
|
||||||
incoming_qty = 0
|
incoming_qty = 0
|
||||||
outgoing_qty = 0
|
outgoing_qty = 0
|
||||||
for p in product.product_variant_ids:
|
for p in template.product_variant_ids:
|
||||||
qty_available += variant_available[p.id]["qty_available"]
|
qty_available += variants_available[p.id]["qty_available"]
|
||||||
virtual_available += variant_available[p.id]["virtual_available"]
|
virtual_available += variants_available[p.id]["virtual_available"]
|
||||||
incoming_qty += variant_available[p.id]["incoming_qty"]
|
incoming_qty += variants_available[p.id]["incoming_qty"]
|
||||||
outgoing_qty += variant_available[p.id]["outgoing_qty"]
|
outgoing_qty += variants_available[p.id]["outgoing_qty"]
|
||||||
prod_available[product.id] = {
|
prod_available[template.id] = {
|
||||||
"qty_available": qty_available,
|
"qty_available": qty_available,
|
||||||
"virtual_available": virtual_available,
|
"virtual_available": virtual_available,
|
||||||
"incoming_qty": incoming_qty,
|
"incoming_qty": incoming_qty,
|
||||||
@@ -372,164 +403,101 @@ class product_template(osv.osv):
|
|||||||
}
|
}
|
||||||
return prod_available
|
return prod_available
|
||||||
|
|
||||||
def _search_product_quantity(self, cr, uid, obj, name, domain, context):
|
def _search_quantities(self, operator, value):
|
||||||
prod = self.pool.get("product.product")
|
# TDE FIXME: does this work anyway ?
|
||||||
product_variant_ids = prod.search(cr, uid, domain, context=context)
|
domain = [] # TDE ADDED
|
||||||
return [('product_variant_ids', 'in', product_variant_ids)]
|
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):
|
def _compute_nbr_reordering_rules(self):
|
||||||
res = {k: {'nbr_reordering_rules': 0, 'reordering_min_qty': 0, 'reordering_max_qty': 0} for k in ids}
|
res = {k: {'nbr_reordering_rules': 0, 'reordering_min_qty': 0, 'reordering_max_qty': 0} for k in self.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)
|
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:
|
for data in product_data:
|
||||||
p = self.pool['product.product'].browse(cr, uid, [data['product_id'][0]], context=context)
|
product = self.env['product.product'].browse([data['product_id'][0]])
|
||||||
product_tmpl_id = p.product_tmpl_id.id
|
product_tmpl_id = product.product_tmpl_id.id
|
||||||
res[product_tmpl_id]['nbr_reordering_rules'] += int(data['product_id_count'])
|
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_min_qty'] = data['product_min_qty']
|
||||||
res[product_tmpl_id]['reordering_max_qty'] = data['product_max_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):
|
@api.model
|
||||||
res = super(product_template, self)._get_product_template_type(cr, uid, context=context)
|
def _get_product_template_type(self):
|
||||||
|
res = super(ProductTemplate, self)._get_product_template_type()
|
||||||
if 'product' not in [item[0] for item in res]:
|
if 'product' not in [item[0] for item in res]:
|
||||||
res.append(('product', _('Stockable Product')))
|
res.append(('product', _('Stockable Product')))
|
||||||
return res
|
return res
|
||||||
|
|
||||||
_columns = {
|
@api.onchange('tracking')
|
||||||
'property_stock_procurement': fields.property(
|
def onchange_tracking(self):
|
||||||
type='many2one',
|
return self.mapped('product_variant_ids').onchange_tracking()
|
||||||
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"),
|
|
||||||
}
|
|
||||||
|
|
||||||
_defaults = {
|
@api.multi
|
||||||
'sale_delay': 0,
|
def write(self, vals):
|
||||||
'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):
|
|
||||||
if 'uom_id' in vals:
|
if 'uom_id' in vals:
|
||||||
new_uom = self.pool.get('product.uom').browse(cr, uid, vals['uom_id'], context=context)
|
new_uom = self.env['product.uom'].browse(vals['uom_id'])
|
||||||
for product in self.browse(cr, uid, ids, context=context):
|
updated = self.filtered(lambda template: template.uom_id != new_uom)
|
||||||
old_uom = product.uom_id
|
done_moves = self.env['stock.move'].search([('product_id', 'in', updated.mapped('product_variant_ids').ids)], limit=1)
|
||||||
if old_uom != new_uom:
|
if done_moves:
|
||||||
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."))
|
||||||
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)
|
||||||
return super(product_template, self).write(cr, uid, ids, vals, context=context)
|
|
||||||
|
@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'
|
_inherit = 'product.category'
|
||||||
|
|
||||||
def calculate_total_routes(self, cr, uid, ids, name, args, context=None):
|
route_ids = fields.Many2many(
|
||||||
res = {}
|
'stock.location.route', 'stock_location_route_categ', 'categ_id', 'route_id', 'Routes',
|
||||||
for categ in self.browse(cr, uid, ids, context=context):
|
domain="[('product_categ_selectable', '=', True)]")
|
||||||
categ2 = categ
|
removal_strategy_id = fields.Many2one(
|
||||||
routes = [x.id for x in categ.route_ids]
|
'product.removal', 'Force Removal Strategy',
|
||||||
while categ2.parent_id:
|
help="Set a specific removal strategy that will be used regardless of the source location for this product category")
|
||||||
categ2 = categ2.parent_id
|
total_route_ids = fields.Many2many(
|
||||||
routes += [x.id for x in categ2.route_ids]
|
'stock.location.route', string='Total routes', compute='_compute_total_route_ids',
|
||||||
res[categ.id] = routes
|
readonly=True)
|
||||||
return res
|
|
||||||
|
|
||||||
_columns = {
|
@api.one
|
||||||
'route_ids': fields.many2many('stock.location.route', 'stock_location_route_categ', 'categ_id', 'route_id', 'Routes', domain="[('product_categ_selectable', '=', True)]"),
|
def _compute_total_route_ids(self):
|
||||||
'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"),
|
category = self
|
||||||
'total_route_ids': fields.function(calculate_total_routes, relation='stock.location.route', type='many2many', string='Total routes', readonly=True),
|
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 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# 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'
|
_name = 'product.removal'
|
||||||
_description = 'Removal Strategy'
|
_description = 'Removal Strategy'
|
||||||
|
|
||||||
_columns = {
|
name = fields.Char('Name', required=True)
|
||||||
'name': fields.char('Name', required=True),
|
method = fields.Char("Method", required=True, help="FIFO, LIFO...")
|
||||||
'method': fields.char("Method", required=True, help="FIFO, LIFO..."),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class product_putaway_strategy(osv.osv):
|
class PutAwayStrategy(models.Model):
|
||||||
_name = 'product.putaway'
|
_name = 'product.putaway'
|
||||||
_description = 'Put Away Strategy'
|
_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')]
|
return [('fixed', 'Fixed Location')]
|
||||||
|
|
||||||
_columns = {
|
def _putaway_apply_fixed(self, product):
|
||||||
'name': fields.char('Name', required=True),
|
for strat in self.fixed_location_ids:
|
||||||
'method': fields.selection(_get_putaway_options, "Method", required=True),
|
categ = product.categ_id
|
||||||
'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),
|
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 = {
|
def putaway_apply(self, product):
|
||||||
'method': 'fixed',
|
if hasattr(self, '_putaway_apply_%s' % (self.method)):
|
||||||
}
|
return getattr(self, '_putaway_apply_%s' % (self.method))
|
||||||
|
return self.env['stock.location']
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class fixed_putaway_strat(osv.osv):
|
class FixedPutAwayStrategy(models.Model):
|
||||||
_name = 'stock.fixed.putaway.strat'
|
_name = 'stock.fixed.putaway.strat'
|
||||||
_order = 'sequence'
|
_order = 'sequence'
|
||||||
_columns = {
|
|
||||||
'putaway_id': fields.many2one('product.putaway', 'Put Away Method', required=True),
|
putaway_id = fields.Many2one('product.putaway', 'Put Away Method', required=True)
|
||||||
'category_id': fields.many2one('product.category', 'Product Category', required=True),
|
category_id = fields.Many2one('product.category', 'Product Category', required=True)
|
||||||
'fixed_location_id': fields.many2one('stock.location', 'Location', 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."),
|
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="inherit_id" ref="product.product_template_form_view"/>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<div name="weight" position="after">
|
<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>
|
</div>
|
||||||
<field name="company_id" position="after">
|
<field name="company_id" position="after">
|
||||||
<field name="route_ids" widget="many2many_checkboxes" attrs="{'invisible':[('type', 'in', ['service', 'digital'])]}"/>
|
<field name="route_ids" widget="many2many_checkboxes" attrs="{'invisible':[('type', 'in', ['service', 'digital'])]}"/>
|
||||||
|
|||||||
Reference in New Issue
Block a user