flectra/addons/stock/models/product.py
2018-07-19 14:21:56 +00:00

612 lines
34 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from flectra import api, fields, models, _
from flectra.addons import decimal_precision as dp
from flectra.exceptions import UserError
from flectra.tools import pycompat
from flectra.tools.float_utils import float_round
from datetime import datetime
import operator as py_operator
OPERATORS = {
'<': py_operator.lt,
'>': py_operator.gt,
'<=': py_operator.le,
'>=': py_operator.ge,
'=': py_operator.eq,
'!=': py_operator.ne
}
class Product(models.Model):
_inherit = "product.product"
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=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=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=dp.get_precision('Product Unit of Measure'),
help="Quantity of planned incoming products.\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=dp.get_precision('Product Unit of Measure'),
help="Quantity of planned outgoing products.\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.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_move_ids.product_qty', 'stock_move_ids.state')
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']
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'))
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
dates_in_the_past = False
if to_date and to_date < fields.Datetime.now(): #Only to_date as to_date will correspond to qty_available
dates_in_the_past = True
domain_move_in = [('product_id', 'in', self.ids)] + domain_move_in_loc
domain_move_out = [('product_id', 'in', self.ids)] + domain_move_out_loc
if lot_id is not None:
domain_quant += [('lot_id', '=', lot_id)]
if owner_id is not None:
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 is not None:
domain_quant += [('package_id', '=', package_id)]
if dates_in_the_past:
domain_move_in_done = list(domain_move_in)
domain_move_out_done = list(domain_move_out)
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)]
Move = self.env['stock.move']
Quant = self.env['stock.quant']
domain_move_in_todo = [('state', 'not in', ('done', 'cancel', 'draft'))] + domain_move_in
domain_move_out_todo = [('state', 'not in', ('done', 'cancel', 'draft'))] + domain_move_out
moves_in_res = dict((item['product_id'][0], item['product_qty']) for item in Move.read_group(domain_move_in_todo, ['product_id', 'product_qty'], ['product_id'], orderby='id'))
moves_out_res = dict((item['product_id'][0], item['product_qty']) for item in Move.read_group(domain_move_out_todo, ['product_id', 'product_qty'], ['product_id'], orderby='id'))
quants_res = dict((item['product_id'][0], item['quantity']) for item in Quant.read_group(domain_quant, ['product_id', 'quantity'], ['product_id'], orderby='id'))
if dates_in_the_past:
# Calculate the moves that were done before now to calculate back in time (as most questions will be recent ones)
domain_move_in_done = [('state', '=', 'done'), ('date', '>', to_date)] + domain_move_in_done
domain_move_out_done = [('state', '=', 'done'), ('date', '>', to_date)] + domain_move_out_done
moves_in_res_past = dict((item['product_id'][0], item['product_qty']) for item in Move.read_group(domain_move_in_done, ['product_id', 'product_qty'], ['product_id'], orderby='id'))
moves_out_res_past = dict((item['product_id'][0], item['product_qty']) for item in Move.read_group(domain_move_out_done, ['product_id', 'product_qty'], ['product_id'], orderby='id'))
res = dict()
for product in self.with_context(prefetch_fields=False):
res[product.id] = {}
if dates_in_the_past:
qty_available = quants_res.get(product.id, 0.0) - moves_in_res_past.get(product.id, 0.0) + moves_out_res_past.get(product.id, 0.0)
else:
qty_available = quants_res.get(product.id, 0.0)
res[product.id]['qty_available'] = float_round(qty_available, 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(
qty_available + res[product.id]['incoming_qty'] - res[product.id]['outgoing_qty'],
precision_rounding=product.uom_id.rounding)
return res
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
'''
Warehouse = self.env['stock.warehouse']
if self.env.context.get('company_owned', False):
company_id = self.env.user.company_id.id
return (
[('location_id.company_id', '=', company_id)],
[('location_id.company_id', '=', False), ('location_dest_id.company_id', '=', company_id)],
[('location_id.company_id', '=', company_id), ('location_dest_id.company_id', '=', False),
])
location_ids = []
if self.env.context.get('location', False):
if isinstance(self.env.context['location'], pycompat.integer_types):
location_ids = [self.env.context['location']]
elif isinstance(self.env.context['location'], pycompat.string_types):
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 = self.env.context['location']
else:
if self.env.context.get('warehouse', False):
if isinstance(self.env.context['warehouse'], pycompat.integer_types):
wids = [self.env.context['warehouse']]
elif isinstance(self.env.context['warehouse'], pycompat.string_types):
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 = self.env.context['warehouse']
else:
wids = Warehouse.search([]).ids
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))
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 = 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:
loc_domain = loc_domain and ['|'] + loc_domain or loc_domain
loc_domain += ['&',
('location_id.parent_left', '>=', location.parent_left),
('location_id.parent_left', '<', location.parent_right)]
dest_loc_domain = dest_loc_domain and ['|'] + dest_loc_domain or dest_loc_domain
dest_loc_domain += ['&',
('location_dest_id.parent_left', '>=', location.parent_left),
('location_dest_id.parent_left', '<', location.parent_right)]
if other_locations:
loc_domain = loc_domain and ['|'] + loc_domain or loc_domain
loc_domain = loc_domain + [('location_id', operator, [location.id for location in other_locations])]
dest_loc_domain = dest_loc_domain and ['|'] + dest_loc_domain or dest_loc_domain
dest_loc_domain = dest_loc_domain + [('location_dest_id', operator, [location.id for location in other_locations])]
return (
domain + loc_domain,
domain + dest_loc_domain + ['!'] + loc_domain if loc_domain else domain + dest_loc_domain,
domain + loc_domain + ['!'] + dest_loc_domain if dest_loc_domain else domain + loc_domain
)
def _search_qty_available(self, operator, value):
# In the very specific case we want to retrieve products with stock available, we only need
# to use the quants, not the stock moves. Therefore, we bypass the usual
# '_search_product_quantity' method and call '_search_qty_available_new' instead. This
# allows better performances.
if value == 0.0 and operator == '>' and not ({'from_date', 'to_date'} & set(self.env.context.keys())):
product_ids = self._search_qty_available_new(
operator, value, self.env.context.get('lot_id'), self.env.context.get('owner_id'),
self.env.context.get('package_id')
)
return [('id', 'in', product_ids)]
return self._search_product_quantity(operator, value, 'qty_available')
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 _search_incoming_qty(self, operator, value):
# TDE FIXME: should probably clean the search methods
return self._search_product_quantity(operator, value, 'incoming_qty')
def _search_outgoing_qty(self, operator, value):
# TDE FIXME: should probably clean the search methods
return self._search_product_quantity(operator, value, 'outgoing_qty')
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 %s') % field)
if operator not in ('<', '>', '=', '!=', '<=', '>='):
raise UserError(_('Invalid domain operator %s') % operator)
if not isinstance(value, (float, int)):
raise UserError(_('Invalid domain right operand %s') % value)
# TODO: Still optimization possible when searching virtual quantities
ids = []
for product in self.with_context(prefetch_fields=False).search([]):
if OPERATORS[operator](product[field], value):
ids.append(product.id)
return [('id', 'in', ids)]
def _search_qty_available_new(self, operator, value, lot_id=False, owner_id=False, package_id=False):
''' Optimized method which doesn't search on stock.moves, only on stock.quants. '''
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', 'quantity'], ['product_id'], orderby='id')
for quant in quants_groupby:
if OPERATORS[operator](quant['quantity'], value):
product_ids.add(quant['product_id'][0])
return list(product_ids)
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 = {i: {} for i in self.ids}
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
@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'], pycompat.integer_types):
location = self.env['stock.location'].browse(self._context['location'])
fields = res.get('fields')
if fields:
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')
elif location.usage == 'internal':
if fields.get('virtual_available'):
res['fields']['virtual_available']['string'] = _('Forecasted Quantity')
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')
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')
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')
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):
return self.mapped('product_tmpl_id').action_view_routes()
def action_view_stock_move_lines(self):
self.ensure_one()
action = self.env.ref('stock.stock_move_line_action').read()[0]
action['domain'] = [('product_id', '=', self.id)]
return action
def action_open_product_lot(self):
self.ensure_one()
action = self.env.ref('stock.action_production_lot_form').read()[0]
action['domain'] = [('product_id', '=', self.id)]
action['context'] = {'default_product_id': self.id}
return action
def write(self, values):
res = super(Product, self).write(values)
if 'active' in values and not values['active']:
products = self.mapped('orderpoint_ids').filtered(lambda r: r.active).mapped('product_id')
if products:
msg = _('You still have some active reordering rules on this product. Please archive or delete them first.')
msg += '\n\n'
for product in products:
msg += '- %s \n' % product.display_name
raise UserError(msg)
return res
class ProductTemplate(models.Model):
_inherit = 'product.template'
responsible_id = fields.Many2one('res.users', string='Responsible', default=lambda self: self.env.uid, required=True)
type = fields.Selection(selection_add=[('product', 'Stockable Product')])
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)
description_pickingout = fields.Text('Description on Delivery Orders', translate=True)
description_pickingin = fields.Text('Description on Receptions', translate=True)
qty_available = fields.Float(
'Quantity On Hand', compute='_compute_quantities', search='_search_qty_available',
digits=dp.get_precision('Product Unit of Measure'))
virtual_available = fields.Float(
'Forecasted Quantity', compute='_compute_quantities', search='_search_virtual_available',
digits=dp.get_precision('Product Unit of Measure'))
incoming_qty = fields.Float(
'Incoming', compute='_compute_quantities', search='_search_incoming_qty',
digits=dp.get_precision('Product Unit of Measure'))
outgoing_qty = fields.Float(
'Outgoing', compute='_compute_quantities', search='_search_outgoing_qty',
digits=dp.get_precision('Product Unit of Measure'))
# The goal of these fields is not to be able to search a location_id/warehouse_id but
# to properly make these fields "dummy": only used to put some keys in context from
# the search view in order to influence computed field
location_id = fields.Many2one('stock.location', 'Location', store=False, search=lambda operator, operand, vals: [])
warehouse_id = fields.Many2one('stock.warehouse', 'Warehouse', store=False, search=lambda operator, operand, vals: [])
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 _is_cost_method_standard(self):
return True
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 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,
"outgoing_qty": outgoing_qty,
}
return prod_available
def _search_qty_available(self, operator, value):
domain = [('qty_available', operator, value)]
product_variant_ids = self.env['product.product'].search(domain)
return [('product_variant_ids', 'in', product_variant_ids.ids)]
def _search_virtual_available(self, operator, value):
domain = [('virtual_available', operator, value)]
product_variant_ids = self.env['product.product'].search(domain)
return [('product_variant_ids', 'in', product_variant_ids.ids)]
def _search_incoming_qty(self, operator, value):
domain = [('incoming_qty', operator, value)]
product_variant_ids = self.env['product.product'].search(domain)
return [('product_variant_ids', 'in', product_variant_ids.ids)]
def _search_outgoing_qty(self, operator, value):
domain = [('outgoing_qty', operator, value)]
product_variant_ids = self.env['product.product'].search(domain)
return [('product_variant_ids', 'in', product_variant_ids.ids)]
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:
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']
for template in self:
template.nbr_reordering_rules = res[template.id]['nbr_reordering_rules']
template.reordering_min_qty = res[template.id]['reordering_min_qty']
template.reordering_max_qty = res[template.id]['reordering_max_qty']
@api.onchange('tracking')
def onchange_tracking(self):
return self.mapped('product_variant_ids').onchange_tracking()
def write(self, vals):
if 'uom_id' in vals:
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.with_context(active_test=False).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."))
if 'type' in vals and vals['type'] != 'product' and sum(self.mapped('nbr_reordering_rules')) != 0:
raise UserError(_('You still have some active reordering rules on this product. Please archive or delete them first.'))
if any('type' in vals and vals['type'] != prod_tmpl.type for prod_tmpl in self):
existing_move_lines = self.env['stock.move.line'].search([
('product_id', 'in', self.mapped('product_variant_ids').ids),
('state', 'in', ['partially_available', 'assigned']),
])
if existing_move_lines:
raise UserError(_("You can not change the type of a product that is currently reserved on a stock move. If you need to change the type, you should first unreserve the stock move."))
return super(ProductTemplate, self).write(vals)
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
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_internal_loc': 1}
return action
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
def action_view_stock_move_lines(self):
self.ensure_one()
action = self.env.ref('stock.stock_move_line_action').read()[0]
action['domain'] = [('product_id.product_tmpl_id', 'in', self.ids)]
return action
def action_open_product_lot(self):
self.ensure_one()
action = self.env.ref('stock.action_production_lot_form').read()[0]
action['domain'] = [('product_id.product_tmpl_id', '=', self.id)]
if self.product_variant_count == 1:
action['context'] = {
'default_product_id': self.product_variant_id.id,
}
return action
class ProductCategory(models.Model):
_inherit = 'product.category'
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)
@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
class ProductUoM(models.Model):
_inherit = 'product.uom'
def write(self, values):
# Users can not update the factor if open stock moves are based on it
if 'factor' in values or 'factor_inv' in values or 'category_id' in values:
changed = self.filtered(
lambda u: any(u[f] != values[f] if f in values else False
for f in {'factor', 'factor_inv'})) + self.filtered(
lambda u: any(u[f].id != int(values[f]) if f in values else False
for f in {'category_id'}))
if changed:
stock_move_lines = self.env['stock.move.line'].search_count([
('product_uom_id.category_id', 'in', changed.mapped('category_id.id')),
('state', '!=', 'cancel'),
])
if stock_move_lines:
raise UserError(_(
"You cannot change the ratio of this unit of mesure as some"
" products with this UoM have already been moved or are "
"currently reserved."
))
return super(ProductUoM, self).write(values)