Instead of having only outgoing, manufacturing, incoming, all... traceability on the product, we can define on the product if there is no tracking, lot tracking or unique serial number tracking. On the picking type we can define whether we will only create lots for the picking type or use existing lots. The lots are required when the product and the picking type demand it. The option for serial number will check that each piece (quantity 1) has its own serial number. A lot splitting wizard is added on the pack operations in the picking form. We want to be able to handle the case where we receive a lot of iPads e.g. and we only assign a serial number when the product is delivered. For this, when the product is delivered, we see that we only have products with no lots. When we see that we take something without lot for something that needed one, we assign the lot to that piece of stock. For negative quants: a positive quant with no lot can reconcile with a negative quant with/without lot, a positive quant with lot, will preferably reconcile with a negative quant with the same lot, afterwards with that without lot.
562 lines
31 KiB
Python
562 lines
31 KiB
Python
# -*- 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
|
|
|
|
class product_product(osv.osv):
|
|
_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']
|
|
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):
|
|
'''
|
|
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')
|
|
|
|
location_ids = []
|
|
if context.get('location', False):
|
|
if type(context['location']) == type(1):
|
|
location_ids = [context['location']]
|
|
elif type(context['location']) in (type(''), type(u'')):
|
|
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)
|
|
else:
|
|
location_ids = context['location']
|
|
else:
|
|
if context.get('warehouse', False):
|
|
wids = [context['warehouse']]
|
|
else:
|
|
wids = warehouse_obj.search(cr, uid, [], context=context)
|
|
|
|
for w in warehouse_obj.browse(cr, uid, wids, context=context):
|
|
location_ids.append(w.view_location_id.id)
|
|
|
|
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)
|
|
if operator == "child_of" and locations and locations[0].parent_left != 0:
|
|
loc_domain = []
|
|
dest_loc_domain = []
|
|
for loc in locations:
|
|
if loc_domain:
|
|
loc_domain = ['|'] + loc_domain + ['&', ('location_id.parent_left', '>=', loc.parent_left), ('location_id.parent_left', '<', loc.parent_right)]
|
|
dest_loc_domain = ['|'] + dest_loc_domain + ['&', ('location_dest_id.parent_left', '>=', loc.parent_left), ('location_dest_id.parent_left', '<', loc.parent_right)]
|
|
else:
|
|
loc_domain += ['&', ('location_id.parent_left', '>=', loc.parent_left), ('location_id.parent_left', '<', loc.parent_right)]
|
|
dest_loc_domain += ['&', ('location_dest_id.parent_left', '>=', loc.parent_left), ('location_dest_id.parent_left', '<', loc.parent_right)]
|
|
|
|
return (
|
|
domain + loc_domain,
|
|
domain + ['&'] + dest_loc_domain + ['!'] + loc_domain,
|
|
domain + ['&'] + loc_domain + ['!'] + dest_loc_domain
|
|
)
|
|
else:
|
|
return (
|
|
domain + [('location_id', operator, location_ids)],
|
|
domain + ['&', ('location_dest_id', operator, location_ids), '!', ('location_id', operator, location_ids)],
|
|
domain + ['&', ('location_id', operator, location_ids), '!', ('location_dest_id', operator, location_ids)]
|
|
)
|
|
|
|
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 _product_available(self, cr, uid, ids, field_names=None, arg=False, context=None):
|
|
context = context or {}
|
|
field_names = field_names or []
|
|
|
|
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
|
|
|
|
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']))
|
|
|
|
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)
|
|
|
|
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))
|
|
|
|
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 = {}
|
|
for product in self.browse(cr, uid, ids, context=context):
|
|
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):
|
|
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'
|
|
|
|
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 _product_available_text(self, cr, uid, ids, field_names=None, arg=False, context=None):
|
|
res = {}
|
|
for product in self.browse(cr, uid, ids, context=context):
|
|
res[product.id] = str(product.qty_available) + _(" On Hand")
|
|
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."),
|
|
'qty_available2': fields.related('qty_available', type="float", relation="product.product", string="On Hand"),
|
|
'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."),
|
|
'location_id': fields.dummy(string='Location', relation='stock.location', type='many2one'),
|
|
'warehouse_id': fields.dummy(string='Warehouse', relation='stock.warehouse', type='many2one'),
|
|
'orderpoint_ids': fields.one2many('stock.warehouse.orderpoint', 'product_id', 'Minimum Stock Rules'),
|
|
}
|
|
|
|
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 ('location' in context) and context['location']:
|
|
location_info = self.pool.get('stock.location').browse(cr, uid, context['location'])
|
|
fields=res.get('fields',{})
|
|
if fields:
|
|
if location_info.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':
|
|
if fields.get('virtual_available'):
|
|
res['fields']['virtual_available']['string'] = _('Future Stock')
|
|
|
|
if location_info.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':
|
|
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':
|
|
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':
|
|
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 {}
|
|
|
|
|
|
class product_template(osv.osv):
|
|
_name = 'product.template'
|
|
_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:
|
|
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] = {
|
|
"qty_available": qty_available,
|
|
"virtual_available": virtual_available,
|
|
"incoming_qty": incoming_qty,
|
|
"outgoing_qty": outgoing_qty,
|
|
}
|
|
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 _product_available_text(self, cr, uid, ids, field_names=None, arg=False, context=None):
|
|
res = {}
|
|
for product in self.browse(cr, uid, ids, context=context):
|
|
res[product.id] = str(product.qty_available) + _(" On Hand")
|
|
return res
|
|
|
|
|
|
|
|
_columns = {
|
|
'type': fields.selection([('product', 'Stockable Product'), ('consu', 'Consumable'), ('service', 'Service')], 'Product Type', required=True, help="Consumable: Will not imply stock management for this product. \nStockable product: Will imply stock management for this product."),
|
|
'qty_available2': fields.related('qty_available', type="float", relation="product.template", string="On Hand"),
|
|
'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."),
|
|
'loc_rack': fields.char('Rack', size=16),
|
|
'loc_row': fields.char('Row', size=16),
|
|
'loc_case': fields.char('Case', size=16),
|
|
'tracking': fields.selection(selection=[('serial', 'By Unique Serial Number'), ('lot', 'By Lots'), ('none', 'No Tracking')], string="Tracking", required=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'),
|
|
|
|
'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,..."),
|
|
}
|
|
|
|
_defaults = {
|
|
'sale_delay': 7,
|
|
'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 len(ids) == 1 and len(products) == 1:
|
|
ctx = "{'tree_view_ref':'stock.view_move_tree', \
|
|
'default_product_id': %s, 'search_default_product_id': %s}" \
|
|
% (products[0], products[0])
|
|
result['context'] = ctx
|
|
else:
|
|
result['domain'] = "[('product_id','in',[" + ','.join(map(str, products)) + "])]"
|
|
result['context'] = "{'tree_view_ref':'stock.view_move_tree'}"
|
|
return result
|
|
|
|
def write(self, cr, uid, ids, vals, context=None):
|
|
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)
|
|
|
|
|
|
class product_removal_strategy(osv.osv):
|
|
_name = 'product.removal'
|
|
_description = 'Removal Strategy'
|
|
|
|
_columns = {
|
|
'name': fields.char('Name', required=True),
|
|
'method': fields.char("Method", required=True, help="FIFO, LIFO..."),
|
|
}
|
|
|
|
|
|
class product_putaway_strategy(osv.osv):
|
|
_name = 'product.putaway'
|
|
_description = 'Put Away Strategy'
|
|
|
|
def _get_putaway_options(self, cr, uid, context=None):
|
|
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),
|
|
}
|
|
|
|
_defaults = {
|
|
'method': 'fixed',
|
|
}
|
|
|
|
def putaway_apply(self, cr, uid, putaway_strat, product, context=None):
|
|
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):
|
|
_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."),
|
|
}
|
|
|
|
|
|
class product_category(osv.osv):
|
|
_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
|
|
|
|
_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),
|
|
}
|