[MIGR] stock: stock_inventory
Inventory 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.
This commit is contained in:
@@ -1,374 +1,354 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import time
|
||||
|
||||
from openerp.addons import decimal_precision as dp
|
||||
from openerp.exceptions import UserError
|
||||
from openerp.osv import fields, osv
|
||||
from openerp.tools.translate import _
|
||||
from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.addons import decimal_precision as dp
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import float_utils
|
||||
|
||||
|
||||
class stock_inventory(osv.osv):
|
||||
class Inventory(models.Model):
|
||||
_name = "stock.inventory"
|
||||
_description = "Inventory"
|
||||
|
||||
def _get_available_filters(self, cr, uid, context=None):
|
||||
"""
|
||||
This function will return the list of filter allowed according to the options checked
|
||||
in 'Settings\Warehouse'.
|
||||
name = fields.Char(
|
||||
'Inventory Reference',
|
||||
readonly=True, required=True,
|
||||
states={'draft': [('readonly', False)]})
|
||||
date = fields.Datetime(
|
||||
'Inventory Date',
|
||||
readonly=True, required=True,
|
||||
default=fields.Datetime.now,
|
||||
help="The date that will be used for the stock level check of the products and the validation of the stock move related to this inventory.")
|
||||
line_ids = fields.One2many(
|
||||
'stock.inventory.line', 'inventory_id', string='Inventories',
|
||||
copy=True, readonly=False,
|
||||
states={'done': [('readonly', True)]})
|
||||
move_ids = fields.One2many(
|
||||
'stock.move', 'inventory_id', string='Created Moves',
|
||||
states={'done': [('readonly', True)]})
|
||||
state = fields.Selection(string='Status', selection=[
|
||||
('draft', 'Draft'),
|
||||
('cancel', 'Cancelled'),
|
||||
('confirm', 'In Progress'),
|
||||
('done', 'Validated')],
|
||||
copy=False, index=True, readonly=True,
|
||||
default='draft')
|
||||
company_id = fields.Many2one(
|
||||
'res.company', 'Company',
|
||||
readonly=True, index=True, required=True,
|
||||
states={'draft': [('readonly', False)]},
|
||||
default=lambda self: self.env['res.company']._company_default_get('stock.inventory'))
|
||||
location_id = fields.Many2one(
|
||||
'stock.location', 'Inventoried Location',
|
||||
readonly=True, required=True,
|
||||
states={'draft': [('readonly', False)]},
|
||||
default=lambda self: getattr(self.env.ref('stock.warehouse0', raise_if_not_found=False) or self.env['stock.warehouse]'], 'lot_stock_id').id)
|
||||
product_id = fields.Many2one(
|
||||
'product.product', 'Inventoried Product',
|
||||
readonly=True,
|
||||
states={'draft': [('readonly', False)]},
|
||||
help="Specify Product to focus your inventory on a particular Product.")
|
||||
package_id = fields.Many2one(
|
||||
'stock.quant.package', 'Inventoried Pack',
|
||||
readonly=True,
|
||||
states={'draft': [('readonly', False)]},
|
||||
help="Specify Pack to focus your inventory on a particular Pack.")
|
||||
partner_id = fields.Many2one(
|
||||
'res.partner', 'Inventoried Owner',
|
||||
readonly=True,
|
||||
states={'draft': [('readonly', False)]},
|
||||
help="Specify Owner to focus your inventory on a particular Owner.")
|
||||
lot_id = fields.Many2one(
|
||||
'stock.production.lot', 'Inventoried Lot/Serial Number',
|
||||
copy=False, readonly=True,
|
||||
states={'draft': [('readonly', False)]},
|
||||
help="Specify Lot/Serial Number to focus your inventory on a particular Lot/Serial Number.")
|
||||
filter = fields.Selection(
|
||||
string='Inventory of', selection='_selection_filter',
|
||||
required=True,
|
||||
default='none',
|
||||
help="If you do an entire inventory, you can choose 'All Products' and it will prefill the inventory with the current stock. If you only do some products "
|
||||
"(e.g. Cycle Counting) you can choose 'Manual Selection of Products' and the system won't propose anything. You can also let the "
|
||||
"system propose for a single product / lot /... ")
|
||||
total_qty = fields.Float('Total Quantity', compute='_compute_total_qty')
|
||||
|
||||
:rtype: list of tuple
|
||||
"""
|
||||
#default available choices
|
||||
res_filter = [('none', _('All products')), ('partial', _('Select products manually')), ('product', _('One product only'))]
|
||||
settings_obj = self.pool.get('stock.config.settings')
|
||||
config_ids = settings_obj.search(cr, uid, [], limit=1, order='id DESC', context=context)
|
||||
#If we don't have updated config until now, all fields are by default false and so should be not dipslayed
|
||||
if not config_ids:
|
||||
@api.one
|
||||
def _compute_total_qty(self):
|
||||
self.total_qty = sum(self.mapped('line_ids').mapped('product_qty'))
|
||||
|
||||
@api.model
|
||||
def _selection_filter(self):
|
||||
""" Get the list of filter allowed according to the options checked
|
||||
in 'Settings\Warehouse'. """
|
||||
res_filter = [
|
||||
('none', _('All products')),
|
||||
('partial', _('Select products manually')),
|
||||
('product', _('One product only'))]
|
||||
stock_settings = self.env['stock.config.settings'].search([], limit=1, order='id DESC')
|
||||
if not stock_settings:
|
||||
return res_filter
|
||||
|
||||
stock_settings = settings_obj.browse(cr, uid, config_ids[0], context=context)
|
||||
if stock_settings.group_stock_tracking_owner:
|
||||
res_filter.append(('owner', _('One owner only')))
|
||||
res_filter.append(('product_owner', _('One product for a specific owner')))
|
||||
res_filter += [('owner', _('One owner only')), ('product_owner', _('One product for a specific owner'))]
|
||||
if stock_settings.group_stock_production_lot:
|
||||
res_filter.append(('lot', _('One Lot/Serial Number')))
|
||||
if stock_settings.group_stock_tracking_lot:
|
||||
res_filter.append(('pack', _('A Pack')))
|
||||
return res_filter
|
||||
|
||||
def _get_total_qty(self, cr, uid, ids, field_name, args, context=None):
|
||||
res = {}
|
||||
for inv in self.browse(cr, uid, ids, context=context):
|
||||
res[inv.id] = sum([x.product_qty for x in inv.line_ids])
|
||||
return res
|
||||
@api.onchange('filter')
|
||||
def onchange_filter(self):
|
||||
if self.filter not in ('product', 'product_owner'):
|
||||
self.product_id = False
|
||||
if self.filter != 'lot':
|
||||
self.lot_id = False
|
||||
if self.filter not in ('owner', 'product_owner'):
|
||||
self.partner_id = False
|
||||
if self.filter != 'pack':
|
||||
self.package_id = False
|
||||
|
||||
_columns = {
|
||||
'name': fields.char('Inventory Reference', required=True, readonly=True, states={'draft': [('readonly', False)]}, help="Inventory Name."),
|
||||
'date': fields.datetime('Inventory Date', required=True, readonly=True, help="The date that will be used for the stock level check of the products and the validation of the stock move related to this inventory."),
|
||||
'line_ids': fields.one2many('stock.inventory.line', 'inventory_id', 'Inventories', readonly=False, states={'done': [('readonly', True)]}, help="Inventory Lines.", copy=True),
|
||||
'move_ids': fields.one2many('stock.move', 'inventory_id', 'Created Moves', help="Inventory Moves.", states={'done': [('readonly', True)]}),
|
||||
'state': fields.selection([
|
||||
('draft', 'Draft'),
|
||||
('cancel', 'Cancelled'),
|
||||
('confirm', 'In Progress'),
|
||||
('done', 'Validated')], 'Status', readonly=True, select=True, copy=False),
|
||||
'company_id': fields.many2one('res.company', 'Company', required=True, select=True, readonly=True, states={'draft': [('readonly', False)]}),
|
||||
'location_id': fields.many2one('stock.location', 'Inventoried Location', required=True, readonly=True, states={'draft': [('readonly', False)]}),
|
||||
'product_id': fields.many2one('product.product', 'Inventoried Product', readonly=True, states={'draft': [('readonly', False)]}, help="Specify Product to focus your inventory on a particular Product."),
|
||||
'package_id': fields.many2one('stock.quant.package', 'Inventoried Pack', readonly=True, states={'draft': [('readonly', False)]}, help="Specify Pack to focus your inventory on a particular Pack."),
|
||||
'partner_id': fields.many2one('res.partner', 'Inventoried Owner', readonly=True, states={'draft': [('readonly', False)]}, help="Specify Owner to focus your inventory on a particular Owner."),
|
||||
'lot_id': fields.many2one('stock.production.lot', 'Inventoried Lot/Serial Number', readonly=True, states={'draft': [('readonly', False)]}, help="Specify Lot/Serial Number to focus your inventory on a particular Lot/Serial Number.", copy=False),
|
||||
'filter': fields.selection(_get_available_filters, 'Inventory of', required=True,
|
||||
help="If you do an entire inventory, you can choose 'All Products' and it will prefill the inventory with the current stock. If you only do some products "\
|
||||
"(e.g. Cycle Counting) you can choose 'Manual Selection of Products' and the system won't propose anything. You can also let the "\
|
||||
"system propose for a single product / lot /... "),
|
||||
'total_qty': fields.function(_get_total_qty, type="float"),
|
||||
}
|
||||
@api.one
|
||||
@api.constrains('filter', 'product_id', 'lot_id', 'partner_id', 'package_id')
|
||||
def _check_filter_product(self):
|
||||
if self.filter == 'none' and self.product_id and self.location_id and self.lot_id:
|
||||
return
|
||||
if self.filter not in ('product', 'product_owner') and self.product_id:
|
||||
raise UserError(_('The selected inventory options are not coherent.'))
|
||||
if self.filter != 'lot' and self.lot_id:
|
||||
raise UserError(_('The selected inventory options are not coherent.'))
|
||||
if self.filter not in ('owner', 'product_owner') and self.partner_id:
|
||||
raise UserError(_('The selected inventory options are not coherent.'))
|
||||
if self.filter != 'pack' and self.package_id:
|
||||
raise UserError(_('The selected inventory options are not coherent.'))
|
||||
|
||||
def _default_stock_location(self, cr, uid, context=None):
|
||||
try:
|
||||
warehouse = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'warehouse0')
|
||||
return warehouse.lot_stock_id.id
|
||||
except:
|
||||
return False
|
||||
@api.multi
|
||||
def action_reset_product_qty(self):
|
||||
self.mapped('line_ids').write({'product_qty': 0})
|
||||
return True
|
||||
reset_real_qty = action_reset_product_qty
|
||||
|
||||
_defaults = {
|
||||
'date': fields.datetime.now,
|
||||
'state': 'draft',
|
||||
'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.inventory', context=c),
|
||||
'location_id': _default_stock_location,
|
||||
'filter': 'none',
|
||||
}
|
||||
|
||||
def reset_real_qty(self, cr, uid, ids, context=None):
|
||||
inventory = self.browse(cr, uid, ids[0], context=context)
|
||||
line_ids = [line.id for line in inventory.line_ids]
|
||||
self.pool.get('stock.inventory.line').write(cr, uid, line_ids, {'product_qty': 0})
|
||||
@api.multi
|
||||
def action_done(self):
|
||||
negative = next((line for line in self.mapped('line_ids') if line.product_qty < 0 and line.product_qty != line.theoretical_qty), False)
|
||||
if negative:
|
||||
raise UserError(_('You cannot set a negative product quantity in an inventory line:\n\t%s - qty: %s') % (negative.product_id.name, negative.product_qty))
|
||||
self.action_check()
|
||||
self.write({'state': 'done'})
|
||||
self.post_inventory()
|
||||
return True
|
||||
|
||||
def action_done(self, cr, uid, ids, context=None):
|
||||
""" Finish the inventory
|
||||
@return: True
|
||||
"""
|
||||
for inv in self.browse(cr, uid, ids, context=context):
|
||||
for inventory_line in inv.line_ids:
|
||||
if inventory_line.product_qty < 0 and inventory_line.product_qty != inventory_line.theoretical_qty:
|
||||
raise UserError(_('You cannot set a negative product quantity in an inventory line:\n\t%s - qty: %s') % (inventory_line.product_id.name, inventory_line.product_qty))
|
||||
self.action_check(cr, uid, [inv.id], context=context)
|
||||
self.write(cr, uid, [inv.id], {'state': 'done'}, context=context)
|
||||
inv.post_inventory()
|
||||
return True
|
||||
@api.multi
|
||||
def post_inventory(self):
|
||||
# The inventory is posted as a single step which means quants cannot be moved from an internal location to another using an inventory
|
||||
# as they will be moved to inventory loss, and other quants will be created to the encoded quant location. This is a normal behavior
|
||||
# as quants cannot be reuse from inventory location (users can still manually move the products before/after the inventory if they want).
|
||||
self.mapped('move_ids').filtered(lambda move: move.state != 'done').action_done()
|
||||
|
||||
def post_inventory(self, cr, uid, ids, context=None):
|
||||
#The inventory is posted as a single step which means quants cannot be moved from an internal location to another using an inventory
|
||||
#as they will be moved to inventory loss, and other quants will be created to the encoded quant location. This is a normal behavior
|
||||
#as quants cannot be reuse from inventory location (users can still manually move the products before/after the inventory if they want).
|
||||
inv = self.browse(cr, uid, ids, context=context)[0]
|
||||
move_obj = self.pool.get('stock.move')
|
||||
move_obj.action_done(cr, uid, [x.id for x in inv.move_ids if x.state != 'done'], context=context)
|
||||
|
||||
def action_check(self, cr, uid, ids, context=None):
|
||||
""" Checks the inventory and computes the stock move to do
|
||||
@return: True
|
||||
"""
|
||||
stock_move_obj = self.pool.get('stock.move')
|
||||
for inventory in self.browse(cr, uid, ids, context=context):
|
||||
#first remove the existing stock moves linked to this inventory
|
||||
move_ids = [move.id for move in inventory.move_ids]
|
||||
stock_move_obj.unlink(cr, uid, move_ids, context=context)
|
||||
@api.multi
|
||||
def action_check(self):
|
||||
""" Checks the inventory and computes the stock move to do """
|
||||
# tde todo: clean after _generate_moves
|
||||
for inventory in self:
|
||||
# first remove the existing stock moves linked to this inventory
|
||||
inventory.mapped('move_ids').unlink()
|
||||
for line in inventory.line_ids:
|
||||
#compare the checked quantities on inventory lines to the theorical one
|
||||
stock_move = line._resolve_inventory_line()
|
||||
# compare the checked quantities on inventory lines to the theorical one
|
||||
stock_move = line._generate_moves()
|
||||
|
||||
def action_cancel_draft(self, cr, uid, ids, context=None):
|
||||
""" Cancels the stock move and change inventory state to draft.
|
||||
@return: True
|
||||
"""
|
||||
for inv in self.browse(cr, uid, ids, context=context):
|
||||
self.write(cr, uid, [inv.id], {'line_ids': [(5,)]}, context=context)
|
||||
self.pool.get('stock.move').action_cancel(cr, uid, [x.id for x in inv.move_ids], context=context)
|
||||
self.write(cr, uid, [inv.id], {'state': 'draft'}, context=context)
|
||||
@api.multi
|
||||
def action_cancel_draft(self):
|
||||
self.mapped('move_ids').action_cancel()
|
||||
self.write({
|
||||
'line_ids': [(5,)],
|
||||
'state': 'draft'
|
||||
})
|
||||
|
||||
@api.multi
|
||||
def action_start(self):
|
||||
for inventory in self.filtered(lambda inventory: not inventory.line_ids and inventory.filter != 'partial'):
|
||||
self.write({
|
||||
'line_ids': [(0, 0, line_values) for line_values in inventory._get_inventory_lines_values()],
|
||||
'state': 'confirm', 'date': fields.Datetime.now()
|
||||
})
|
||||
return True
|
||||
prepare_inventory = action_start
|
||||
|
||||
def prepare_inventory(self, cr, uid, ids, context=None):
|
||||
inventory_line_obj = self.pool.get('stock.inventory.line')
|
||||
for inventory in self.browse(cr, uid, ids, context=context):
|
||||
# If there are inventory lines already (e.g. from import), respect those and set their theoretical qty
|
||||
line_ids = [line.id for line in inventory.line_ids]
|
||||
if not line_ids and inventory.filter != 'partial':
|
||||
#compute the inventory lines and create them
|
||||
vals = inventory._get_inventory_lines()
|
||||
for product_line in vals:
|
||||
inventory_line_obj.create(cr, uid, product_line, context=context)
|
||||
return self.write(cr, uid, ids, {'state': 'confirm', 'date': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)})
|
||||
|
||||
def _get_inventory_lines(self, cr, uid, ids, context=None):
|
||||
inventory = self.browse(cr, uid, ids, context=context)[0]
|
||||
location_obj = self.pool.get('stock.location')
|
||||
product_obj = self.pool.get('product.product')
|
||||
location_ids = location_obj.search(cr, uid, [('id', 'child_of', [inventory.location_id.id])], context=context)
|
||||
@api.multi
|
||||
def _get_inventory_lines_values(self):
|
||||
# TDE CLEANME: is sql really necessary ? I don't think so
|
||||
locations = self.env['stock.location'].search([('id', 'child_of', [self.location_id.id])])
|
||||
domain = ' location_id in %s'
|
||||
args = (tuple(location_ids),)
|
||||
if inventory.partner_id:
|
||||
domain += ' and owner_id = %s'
|
||||
args += (inventory.partner_id.id,)
|
||||
if inventory.lot_id:
|
||||
domain += ' and lot_id = %s'
|
||||
args += (inventory.lot_id.id,)
|
||||
if inventory.product_id:
|
||||
domain += ' and product_id = %s'
|
||||
args += (inventory.product_id.id,)
|
||||
if inventory.package_id:
|
||||
domain += ' and package_id = %s'
|
||||
args += (inventory.package_id.id,)
|
||||
args = (tuple(locations.ids),)
|
||||
if self.partner_id:
|
||||
domain += ' AND owner_id = %s'
|
||||
args += (self.partner_id.id,)
|
||||
if self.lot_id:
|
||||
domain += ' AND lot_id = %s'
|
||||
args += (self.lot_id.id,)
|
||||
if self.product_id:
|
||||
domain += ' AND product_id = %s'
|
||||
args += (self.product_id.id,)
|
||||
if self.package_id:
|
||||
domain += ' AND package_id = %s'
|
||||
args += (self.package_id.id,)
|
||||
|
||||
cr.execute('''
|
||||
SELECT product_id, sum(qty) as product_qty, location_id, lot_id as prod_lot_id, package_id, owner_id as partner_id
|
||||
FROM stock_quant WHERE''' + domain + '''
|
||||
GROUP BY product_id, location_id, lot_id, package_id, partner_id
|
||||
''', args)
|
||||
Product = self.env['product.product']
|
||||
vals = []
|
||||
for product_line in cr.dictfetchall():
|
||||
#replace the None the dictionary by False, because falsy values are tested later on
|
||||
for key, value in product_line.items():
|
||||
if not value:
|
||||
product_line[key] = False
|
||||
product_line['inventory_id'] = inventory.id
|
||||
product_line['theoretical_qty'] = product_line['product_qty']
|
||||
if product_line['product_id']:
|
||||
product = product_obj.browse(cr, uid, product_line['product_id'], context=context)
|
||||
product_line['product_uom_id'] = product.uom_id.id
|
||||
vals.append(product_line)
|
||||
self.env.cr.execute("""SELECT product_id, sum(qty) as product_qty, location_id, lot_id as prod_lot_id, package_id, owner_id as partner_id
|
||||
FROM stock_quant
|
||||
WHERE %s
|
||||
GROUP BY product_id, location_id, lot_id, package_id, partner_id """ % domain, args)
|
||||
for product_data in self.env.cr.dictfetchall():
|
||||
# replace the None the dictionary by False, because falsy values are tested later on
|
||||
for void_field in [item[0] for item in product_data.items() if item[1] is None]:
|
||||
product_data[void_field] = False
|
||||
product_data['theoretical_qty'] = product_data['product_qty']
|
||||
if product_data['product_id']:
|
||||
product_data['product_uom_id'] = Product.browse(product_data['product_id']).uom_id.id
|
||||
vals.append(product_data)
|
||||
return vals
|
||||
|
||||
def _check_filter_product(self, cr, uid, ids, context=None):
|
||||
for inventory in self.browse(cr, uid, ids, context=context):
|
||||
if inventory.filter == 'none' and inventory.product_id and inventory.location_id and inventory.lot_id:
|
||||
return True
|
||||
if inventory.filter not in ('product', 'product_owner') and inventory.product_id:
|
||||
return False
|
||||
if inventory.filter != 'lot' and inventory.lot_id:
|
||||
return False
|
||||
if inventory.filter not in ('owner', 'product_owner') and inventory.partner_id:
|
||||
return False
|
||||
if inventory.filter != 'pack' and inventory.package_id:
|
||||
return False
|
||||
return True
|
||||
|
||||
def onchange_filter(self, cr, uid, ids, filter, context=None):
|
||||
to_clean = { 'value': {} }
|
||||
if filter not in ('product', 'product_owner'):
|
||||
to_clean['value']['product_id'] = False
|
||||
if filter != 'lot':
|
||||
to_clean['value']['lot_id'] = False
|
||||
if filter not in ('owner', 'product_owner'):
|
||||
to_clean['value']['partner_id'] = False
|
||||
if filter != 'pack':
|
||||
to_clean['value']['package_id'] = False
|
||||
return to_clean
|
||||
|
||||
_constraints = [
|
||||
(_check_filter_product, 'The selected inventory options are not coherent.',
|
||||
['filter', 'product_id', 'lot_id', 'partner_id', 'package_id']),
|
||||
]
|
||||
|
||||
class stock_inventory_line(osv.osv):
|
||||
class InventoryLine(models.Model):
|
||||
_name = "stock.inventory.line"
|
||||
_description = "Inventory Line"
|
||||
_order = "inventory_id, location_name, product_code, product_name, prodlot_name"
|
||||
|
||||
def _get_product_name_change(self, cr, uid, ids, context=None):
|
||||
return self.pool.get('stock.inventory.line').search(cr, uid, [('product_id', 'in', ids)], context=context)
|
||||
inventory_id = fields.Many2one(
|
||||
'stock.inventory', 'Inventory',
|
||||
index=True, ondelete='cascade')
|
||||
partner_id = fields.Many2one('res.partner', 'Owner')
|
||||
product_id = fields.Many2one(
|
||||
'product.product', 'Product',
|
||||
index=True, required=True)
|
||||
product_name = fields.Char(
|
||||
'Product Name', related='product_id.name', store=True)
|
||||
product_code = fields.Char(
|
||||
'Product Code', related='product_id.default_code', store=True)
|
||||
product_uom_id = fields.Many2one(
|
||||
'product.uom', 'Product Unit of Measure',
|
||||
required=True,
|
||||
default=lambda self: self.env.ref('product.product_uom_unit', raise_if_not_found=True))
|
||||
product_qty = fields.Float(
|
||||
'Checked Quantity',
|
||||
digits_compute=dp.get_precision('Product Unit of Measure'), default=0)
|
||||
location_id = fields.Many2one(
|
||||
'stock.location', 'Location',
|
||||
index=True, required=True)
|
||||
# TDE FIXME: necessary ? only in order -> replace by location_id
|
||||
location_name = fields.Char(
|
||||
'Location Name', related='location_id.complete_name', store=True)
|
||||
package_id = fields.Many2one(
|
||||
'stock.quant.package', 'Pack', index=True)
|
||||
prod_lot_id = fields.Many2one(
|
||||
'stock.production.lot', 'Serial Number',
|
||||
domain="[('product_id','=',product_id)]")
|
||||
# TDE FIXME: necessary ? -> replace by location_id
|
||||
prodlot_name = fields.Char(
|
||||
'Serial Number Name',
|
||||
related='prod_lot_id.name', store=True)
|
||||
company_id = fields.Many2one(
|
||||
'res.company', 'Company', related='inventory_id.company_id',
|
||||
index=True, readonly=True, store=True)
|
||||
# TDE FIXME: necessary ? -> replace by location_id
|
||||
state = fields.Selection(
|
||||
'Status', related='inventory_id.state', readonly=True)
|
||||
theoretical_qty = fields.Float(
|
||||
'Theoretical Quantity', compute='_compute_theoretical_qty',
|
||||
digits_compute=dp.get_precision('Product Unit of Measure'), readonly=True, store=True)
|
||||
|
||||
def _get_location_change(self, cr, uid, ids, context=None):
|
||||
return self.pool.get('stock.inventory.line').search(cr, uid, [('location_id', 'in', ids)], context=context)
|
||||
|
||||
def _get_prodlot_change(self, cr, uid, ids, context=None):
|
||||
return self.pool.get('stock.inventory.line').search(cr, uid, [('prod_lot_id', 'in', ids)], context=context)
|
||||
|
||||
def _get_theoretical_qty(self, cr, uid, ids, name, args, context=None):
|
||||
res = {}
|
||||
quant_obj = self.pool["stock.quant"]
|
||||
uom_obj = self.pool["product.uom"]
|
||||
for line in self.browse(cr, uid, ids, context=context):
|
||||
quant_ids = line._get_quants()
|
||||
quants = quant_obj.browse(cr, uid, quant_ids, context=context)
|
||||
tot_qty = sum([x.qty for x in quants])
|
||||
if line.product_uom_id and line.product_id.uom_id.id != line.product_uom_id.id:
|
||||
tot_qty = uom_obj._compute_qty_obj(cr, uid, line.product_id.uom_id, tot_qty, line.product_uom_id, context=context)
|
||||
res[line.id] = tot_qty
|
||||
return res
|
||||
|
||||
_columns = {
|
||||
'inventory_id': fields.many2one('stock.inventory', 'Inventory', ondelete='cascade', select=True),
|
||||
'location_id': fields.many2one('stock.location', 'Location', required=True, select=True),
|
||||
'product_id': fields.many2one('product.product', 'Product', required=True, select=True),
|
||||
'package_id': fields.many2one('stock.quant.package', 'Pack', select=True),
|
||||
'product_uom_id': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
|
||||
'product_qty': fields.float('Checked Quantity', digits_compute=dp.get_precision('Product Unit of Measure')),
|
||||
'company_id': fields.related('inventory_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, select=True, readonly=True),
|
||||
'prod_lot_id': fields.many2one('stock.production.lot', 'Serial Number', domain="[('product_id','=',product_id)]"),
|
||||
'state': fields.related('inventory_id', 'state', type='char', string='Status', readonly=True),
|
||||
'theoretical_qty': fields.function(_get_theoretical_qty, type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
|
||||
store={'stock.inventory.line': (lambda self, cr, uid, ids, c={}: ids, ['location_id', 'product_id', 'package_id', 'product_uom_id', 'company_id', 'prod_lot_id', 'partner_id'], 20),},
|
||||
readonly=True, string="Theoretical Quantity"),
|
||||
'partner_id': fields.many2one('res.partner', 'Owner'),
|
||||
'product_name': fields.related('product_id', 'name', type='char', string='Product Name', store={
|
||||
'product.product': (_get_product_name_change, ['name', 'default_code'], 20),
|
||||
'stock.inventory.line': (lambda self, cr, uid, ids, c={}: ids, ['product_id'], 20),}),
|
||||
'product_code': fields.related('product_id', 'default_code', type='char', string='Product Code', store={
|
||||
'product.product': (_get_product_name_change, ['name', 'default_code'], 20),
|
||||
'stock.inventory.line': (lambda self, cr, uid, ids, c={}: ids, ['product_id'], 20),}),
|
||||
'location_name': fields.related('location_id', 'complete_name', type='char', string='Location Name', store={
|
||||
'stock.location': (_get_location_change, ['name', 'location_id', 'active'], 20),
|
||||
'stock.inventory.line': (lambda self, cr, uid, ids, c={}: ids, ['location_id'], 20),}),
|
||||
'prodlot_name': fields.related('prod_lot_id', 'name', type='char', string='Serial Number Name', store={
|
||||
'stock.production.lot': (_get_prodlot_change, ['name'], 20),
|
||||
'stock.inventory.line': (lambda self, cr, uid, ids, c={}: ids, ['prod_lot_id'], 20),}),
|
||||
}
|
||||
|
||||
_defaults = {
|
||||
'product_qty': 0,
|
||||
'product_uom_id': lambda self, cr, uid, ctx=None: self.pool['ir.model.data'].get_object_reference(cr, uid, 'product', 'product_uom_unit')[1]
|
||||
}
|
||||
|
||||
def create(self, cr, uid, values, context=None):
|
||||
product_obj = self.pool.get('product.product')
|
||||
dom = [('product_id', '=', values.get('product_id')), ('inventory_id.state', '=', 'confirm'),
|
||||
('location_id', '=', values.get('location_id')), ('partner_id', '=', values.get('partner_id')),
|
||||
('package_id', '=', values.get('package_id')), ('prod_lot_id', '=', values.get('prod_lot_id'))]
|
||||
res = self.search(cr, uid, dom, context=context)
|
||||
if res:
|
||||
location = self.pool['stock.location'].browse(cr, uid, values.get('location_id'), context=context)
|
||||
product = product_obj.browse(cr, uid, values.get('product_id'), context=context)
|
||||
raise UserError(_("You cannot have two inventory adjustements in state 'in Progess' with the same product(%s), same location(%s), same package, same owner and same lot. Please first validate the first inventory adjustement with this product before creating another one.") % (product.name, location.name))
|
||||
if 'product_id' in values and not 'product_uom_id' in values:
|
||||
values['product_uom_id'] = product_obj.browse(cr, uid, values.get('product_id'), context=context).uom_id.id
|
||||
return super(stock_inventory_line, self).create(cr, uid, values, context=context)
|
||||
|
||||
def _get_quants(self, cr, uid, ids, context=None):
|
||||
line = self.browse(cr, uid, ids, context=context)[0]
|
||||
quant_obj = self.pool["stock.quant"]
|
||||
dom = [('company_id', '=', line.company_id.id), ('location_id', '=', line.location_id.id), ('lot_id', '=', line.prod_lot_id.id),
|
||||
('product_id','=', line.product_id.id), ('owner_id', '=', line.partner_id.id), ('package_id', '=', line.package_id.id)]
|
||||
quants = quant_obj.search(cr, uid, dom, context=context)
|
||||
return quants
|
||||
|
||||
def onchange_createline(self, cr, uid, ids, location_id=False, product_id=False, uom_id=False, package_id=False, prod_lot_id=False, partner_id=False, company_id=False, context=None):
|
||||
quant_obj = self.pool["stock.quant"]
|
||||
uom_obj = self.pool["product.uom"]
|
||||
res = {'value': {}}
|
||||
# If no UoM already put the default UoM of the product
|
||||
if product_id:
|
||||
product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
|
||||
uom = self.pool['product.uom'].browse(cr, uid, uom_id, context=context)
|
||||
if product.uom_id.category_id.id != uom.category_id.id:
|
||||
res['value']['product_uom_id'] = product.uom_id.id
|
||||
res['domain'] = {'product_uom_id': [('category_id','=',product.uom_id.category_id.id)]}
|
||||
uom_id = product.uom_id.id
|
||||
# Calculate theoretical quantity by searching the quants as in quants_get
|
||||
if product_id and location_id:
|
||||
product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
|
||||
if not company_id:
|
||||
company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
|
||||
dom = [('company_id', '=', company_id), ('location_id', '=', location_id), ('lot_id', '=', prod_lot_id),
|
||||
('product_id','=', product_id), ('owner_id', '=', partner_id), ('package_id', '=', package_id)]
|
||||
quants = quant_obj.search(cr, uid, dom, context=context)
|
||||
th_qty = sum([x.qty for x in quant_obj.browse(cr, uid, quants, context=context)])
|
||||
if product_id and uom_id and product.uom_id.id != uom_id:
|
||||
th_qty = uom_obj._compute_qty(cr, uid, product.uom_id.id, th_qty, uom_id)
|
||||
res['value']['theoretical_qty'] = th_qty
|
||||
res['value']['product_qty'] = th_qty
|
||||
return res
|
||||
|
||||
def _resolve_inventory_line(self, cr, uid, ids, context=None):
|
||||
inventory_line = self.browse(cr, uid, ids, context=context)[0]
|
||||
stock_move_obj = self.pool.get('stock.move')
|
||||
quant_obj = self.pool.get('stock.quant')
|
||||
diff = inventory_line.theoretical_qty - inventory_line.product_qty
|
||||
if not diff:
|
||||
@api.one
|
||||
@api.depends('location_id', 'product_id', 'package_id', 'product_uom_id', 'company_id', 'prod_lot_id', 'partner_id')
|
||||
def _compute_theoretical_qty(self):
|
||||
if not self.product_id:
|
||||
self.theoretical_qty = 0
|
||||
return
|
||||
#each theorical_lines where difference between theoretical and checked quantities is not 0 is a line for which we need to create a stock move
|
||||
vals = {
|
||||
'name': _('INV:') + (inventory_line.inventory_id.name or ''),
|
||||
'product_id': inventory_line.product_id.id,
|
||||
'product_uom': inventory_line.product_uom_id.id,
|
||||
'date': inventory_line.inventory_id.date,
|
||||
'company_id': inventory_line.inventory_id.company_id.id,
|
||||
'inventory_id': inventory_line.inventory_id.id,
|
||||
'state': 'confirmed',
|
||||
'restrict_lot_id': inventory_line.prod_lot_id.id,
|
||||
'restrict_partner_id': inventory_line.partner_id.id,
|
||||
}
|
||||
inventory_location_id = inventory_line.product_id.property_stock_inventory.id
|
||||
if diff < 0:
|
||||
#found more than expected
|
||||
vals['location_id'] = inventory_location_id
|
||||
vals['location_dest_id'] = inventory_line.location_id.id
|
||||
vals['product_uom_qty'] = -diff
|
||||
else:
|
||||
#found less than expected
|
||||
vals['location_id'] = inventory_line.location_id.id
|
||||
vals['location_dest_id'] = inventory_location_id
|
||||
vals['product_uom_qty'] = diff
|
||||
move_id = stock_move_obj.create(cr, uid, vals, context=context)
|
||||
move = stock_move_obj.browse(cr, uid, move_id, context=context)
|
||||
if diff > 0:
|
||||
domain = [('qty', '>', 0.0), ('package_id', '=', inventory_line.package_id.id), ('lot_id', '=', inventory_line.prod_lot_id.id), ('location_id', '=', inventory_line.location_id.id)]
|
||||
preferred_domain_list = [[('reservation_id', '=', False)], [('reservation_id.inventory_id', '!=', inventory_line.inventory_id.id)]]
|
||||
quants = quant_obj.quants_get_preferred_domain(cr, uid, move.product_qty, move, domain=domain, preferred_domain_list=preferred_domain_list)
|
||||
quant_obj.quants_reserve(cr, uid, quants, move, context=context)
|
||||
elif inventory_line.package_id:
|
||||
stock_move_obj.action_done(cr, uid, move_id, context=context)
|
||||
quants = [x.id for x in move.quant_ids]
|
||||
quant_obj.write(cr, uid, quants, {'package_id': inventory_line.package_id.id}, context=context)
|
||||
res = quant_obj.search(cr, uid, [('qty', '<', 0.0), ('product_id', '=', move.product_id.id),
|
||||
('location_id', '=', move.location_dest_id.id), ('package_id', '!=', False)], limit=1, context=context)
|
||||
if res:
|
||||
for quant in move.quant_ids:
|
||||
if quant.location_id.id == move.location_dest_id.id: #To avoid we take a quant that was reconcile already
|
||||
quant_obj._quant_reconcile_negative(cr, uid, quant, move, context=context)
|
||||
return move_id
|
||||
theoretical_qty = sum([x.qty for x in self._get_quants()])
|
||||
if theoretical_qty and self.product_uom_id and self.product_id.uom_id != self.product_uom_id:
|
||||
theoretical_qty = self.env["product.uom"]._compute_qty_obj(self.product_id.uom_id, theoretical_qty, self.product_uom_id)
|
||||
self.theoretical_qty = theoretical_qty
|
||||
|
||||
@api.onchange('product_id', 'product_uom_id')
|
||||
def onchange_product_or_uom(self):
|
||||
res = {}
|
||||
# If no UoM or incorrect UoM put default one from product
|
||||
if self.product_id and (not self.product_uom_id or self.product_id.uom_id.category_id != self.product_uom_id.category_id):
|
||||
self.product_uom_id = self.product_id.uom_id
|
||||
res['domain'] = {'product_uom_id': [('category_id', '=', self.product_id.uom_id.category_id.id)]}
|
||||
return res
|
||||
|
||||
@api.onchange('product_id', 'location_id', 'product_uom_id', 'prod_lot_id', 'partner_id', 'package_id')
|
||||
def onchange_quantity_context(self):
|
||||
if self.product_id and self.location_id and self.product_id.uom_id.category_id == self.product_uom_id.category_id: # TDE FIXME: last part added because crash
|
||||
self._compute_theoretical_qty()
|
||||
self.product_qty = self.theoretical_qty
|
||||
|
||||
@api.model
|
||||
def create(self, values):
|
||||
if 'product_id' in values and 'product_uom_id' not in values:
|
||||
values['product_uom_id'] = self.env['product.product'].browse(values['product_id']).uom_id.id
|
||||
existings = self.search([
|
||||
('product_id', '=', values.get('product_id')),
|
||||
('inventory_id.state', '=', 'confirm'),
|
||||
('location_id', '=', values.get('location_id')),
|
||||
('partner_id', '=', values.get('partner_id')),
|
||||
('package_id', '=', values.get('package_id')),
|
||||
('prod_lot_id', '=', values.get('prod_lot_id'))])
|
||||
res = super(InventoryLine, self).create(values)
|
||||
if existings:
|
||||
raise UserError(_("You cannot have two inventory adjustements in state 'in Progess' with the same product"
|
||||
"(%s), same location(%s), same package, same owner and same lot. Please first validate"
|
||||
"the first inventory adjustement with this product before creating another one.") % (res.product_id.name, res.location_id.name))
|
||||
return res
|
||||
|
||||
def _get_quants(self):
|
||||
return self.env['stock.quant'].search([
|
||||
('company_id', '=', self.company_id.id),
|
||||
('location_id', '=', self.location_id.id),
|
||||
('lot_id', '=', self.prod_lot_id.id),
|
||||
('product_id', '=', self.product_id.id),
|
||||
('owner_id', '=', self.partner_id.id),
|
||||
('package_id', '=', self.package_id.id)])
|
||||
|
||||
def _generate_moves(self):
|
||||
moves = self.env['stock.move']
|
||||
Quant = self.env['stock.quant']
|
||||
for line in self:
|
||||
if float_utils.float_compare(line.theoretical_qty, line.product_qty, precision_rounding=line.product_id.uom_id.rounding) == 0:
|
||||
continue
|
||||
diff = line.theoretical_qty - line.product_qty
|
||||
vals = {
|
||||
'name': _('INV:') + (line.inventory_id.name or ''),
|
||||
'product_id': line.product_id.id,
|
||||
'product_uom': line.product_uom_id.id,
|
||||
'date': line.inventory_id.date,
|
||||
'company_id': line.inventory_id.company_id.id,
|
||||
'inventory_id': line.inventory_id.id,
|
||||
'state': 'confirmed',
|
||||
'restrict_lot_id': line.prod_lot_id.id,
|
||||
'restrict_partner_id': line.partner_id.id}
|
||||
if diff < 0: # found more than expected
|
||||
vals['location_id'] = line.product_id.property_stock_inventory.id
|
||||
vals['location_dest_id'] = line.location_id.id
|
||||
vals['product_uom_qty'] = abs(diff)
|
||||
else:
|
||||
vals['location_id'] = line.location_id.id
|
||||
vals['location_dest_id'] = line.product_id.property_stock_inventory.id
|
||||
vals['product_uom_qty'] = diff
|
||||
move = moves.create(vals)
|
||||
|
||||
if diff > 0:
|
||||
domain = [('qty', '>', 0.0), ('package_id', '=', line.package_id.id), ('lot_id', '=', line.prod_lot_id.id), ('location_id', '=', line.location_id.id)]
|
||||
preferred_domain_list = [[('reservation_id', '=', False)], [('reservation_id.inventory_id', '!=', line.inventory_id.id)]]
|
||||
quants = Quant.quants_get_preferred_domain(move.product_qty, move, domain=domain, preferred_domain_list=preferred_domain_list)
|
||||
Quant.quants_reserve(quants, move)
|
||||
elif line.package_id:
|
||||
move.action_done()
|
||||
move.quant_ids.write({'package_id': line.package_id.id})
|
||||
quants = Quant.search([('qty', '<', 0.0), ('product_id', '=', move.product_id.id),
|
||||
('location_id', '=', move.location_dest_id.id), ('package_id', '!=', False)], limit=1)
|
||||
if quants:
|
||||
for quant in move.quant_ids:
|
||||
if quant.location_id.id == move.location_dest_id.id: #To avoid we take a quant that was reconcile already
|
||||
quant._quant_reconcile_negative(move)
|
||||
return moves
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
<group>
|
||||
<group>
|
||||
<field name="location_id" domain="[('usage','not in', ['supplier','production'])]"/>
|
||||
<field name="filter" string="Inventory of" widget='radio' attrs="{'readonly': [('state', '!=', 'draft')]}" on_change="onchange_filter(filter)"/>
|
||||
<field name="filter" string="Inventory of" widget='radio' attrs="{'readonly': [('state', '!=', 'draft')]}"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="date"/>
|
||||
@@ -79,23 +79,23 @@
|
||||
<button name="reset_real_qty" states="confirm" string="⇒ Set quantities to 0" type="object" class="oe_link oe_right" groups="stock.group_stock_user"/>
|
||||
<field name="line_ids" string="Inventory Details" context="{'default_location_id': location_id, 'default_product_id': product_id, 'default_prod_lot_id': lot_id, 'default_package_id': package_id, 'default_partner_id': partner_id}" mode="tree,kanban">
|
||||
<tree string="Inventory Details" editable="bottom" decoration-info="product_qty != theoretical_qty" decoration-danger="theoretical_qty < 0">
|
||||
<field name="product_id" domain="[('type','=','product')]" on_change="onchange_createline(location_id, product_id, product_uom_id, package_id, prod_lot_id, partner_id)"/>
|
||||
<field name="product_uom_id" groups="product.group_uom" string="UoM" on_change="onchange_createline(location_id, product_id, product_uom_id, package_id, prod_lot_id, partner_id)"/>
|
||||
<field name="location_id" domain="[('id', 'child_of', parent.location_id)]" groups="stock.group_stock_multi_locations" on_change="onchange_createline(location_id, product_id, product_uom_id, package_id, prod_lot_id, partner_id)"/>
|
||||
<field name="prod_lot_id" on_change="onchange_createline(location_id, product_id, product_uom_id, package_id, prod_lot_id, partner_id)" domain="[('product_id', '=', product_id)]" context="{'default_product_id': product_id}" groups="stock.group_production_lot"/>
|
||||
<field name="package_id" domain="['|', ('location_id','=', False), ('location_id', '=', location_id)]" on_change="onchange_createline(location_id, product_id, product_uom_id, package_id, prod_lot_id, partner_id)" groups="stock.group_tracking_lot"/>
|
||||
<field name="partner_id" on_change="onchange_createline(location_id, product_id, product_uom_id, package_id, prod_lot_id, partner_id)" groups="stock.group_tracking_owner"/>
|
||||
<field name="product_id" domain="[('type','=','product')]"/>
|
||||
<field name="product_uom_id" string="UoM" groups="product.group_uom"/>
|
||||
<field name="location_id" domain="[('id', 'child_of', parent.location_id)]" groups="stock.group_stock_multi_locations"/>
|
||||
<field name="prod_lot_id" domain="[('product_id', '=', product_id)]" context="{'default_product_id': product_id}" groups="stock.group_production_lot"/>
|
||||
<field name="package_id" domain="['|', ('location_id','=', False), ('location_id', '=', location_id)]" groups="stock.group_tracking_lot"/>
|
||||
<field name="partner_id" groups="stock.group_tracking_owner"/>
|
||||
<field name="theoretical_qty" readonly="1"/>
|
||||
<field name="product_qty" string="Real Quantity"/>
|
||||
<field name="state" invisible="True"/>
|
||||
</tree>
|
||||
<kanban class="o_kanban_mobile">
|
||||
<field name="product_id" domain="[('type','=','product')]" on_change="onchange_createline(location_id, product_id, product_uom_id, package_id, prod_lot_id, partner_id)"/>
|
||||
<field name="product_uom_id" groups="product.group_uom" string="UoM" on_change="onchange_createline(location_id, product_id, product_uom_id, package_id, prod_lot_id, partner_id)"/>
|
||||
<field name="location_id" domain="[('id', 'child_of', parent.location_id)]" groups="stock.group_locations" on_change="onchange_createline(location_id, product_id, product_uom_id, package_id, prod_lot_id, partner_id)"/>
|
||||
<field name="prod_lot_id" on_change="onchange_createline(location_id, product_id, product_uom_id, package_id, prod_lot_id, partner_id)" domain="[('product_id', '=', product_id)]" context="{'default_product_id': product_id}" groups="stock.group_production_lot"/>
|
||||
<field name="package_id" domain="['|', ('location_id','=', False), ('location_id', '=', location_id)]" on_change="onchange_createline(location_id, product_id, product_uom_id, package_id, prod_lot_id, partner_id)" groups="stock.group_tracking_lot"/>
|
||||
<field name="partner_id" on_change="onchange_createline(location_id, product_id, product_uom_id, package_id, prod_lot_id, partner_id)" groups="stock.group_tracking_owner"/>
|
||||
<field name="product_id" domain="[('type','=','product')]"/>
|
||||
<field name="product_uom_id" groups="product.group_uom" string="UoM"/>
|
||||
<field name="location_id" domain="[('id', 'child_of', parent.location_id)]" groups="stock.group_locations"/>
|
||||
<field name="prod_lot_id" domain="[('product_id', '=', product_id)]" context="{'default_product_id': product_id}" groups="stock.group_production_lot"/>
|
||||
<field name="package_id" domain="['|', ('location_id','=', False), ('location_id', '=', location_id)]"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="theoretical_qty" readonly="1"/>
|
||||
<field name="product_qty" string="Real Quantity"/>
|
||||
<field name="state" invisible="True"/>
|
||||
|
||||
Reference in New Issue
Block a user