[MIGR] stock: stock_pack_operation
Pack operation 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,365 +1,302 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from openerp.osv import fields, osv
|
||||
from openerp.tools.float_utils import float_round
|
||||
from openerp.tools.translate import _
|
||||
from openerp import api, models
|
||||
import openerp.addons.decimal_precision as dp
|
||||
from openerp.exceptions import UserError
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
from odoo.addons import decimal_precision as dp
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.tools.float_utils import float_round, float_compare
|
||||
|
||||
|
||||
from openerp.report import report_sxw
|
||||
|
||||
class stock_pack_operation(osv.osv):
|
||||
class PackOperation(models.Model):
|
||||
_name = "stock.pack.operation"
|
||||
_description = "Packing Operation"
|
||||
|
||||
_order = "result_package_id desc, id"
|
||||
|
||||
def _get_remaining_prod_quantities(self, cr, uid, ids, context=None):
|
||||
# TDE FIXME: strange, probably to remove
|
||||
def _get_default_from_loc(self):
|
||||
default_loc = self.env.context.get('default_location_id')
|
||||
if default_loc:
|
||||
return self.env['stock.location'].browse(default_loc).name
|
||||
|
||||
# TDE FIXME: strange, probably to remove
|
||||
def _get_default_to_loc(self):
|
||||
default_loc = self.env.context.get('default_location_dest_id')
|
||||
if default_loc:
|
||||
return self.env['stock.location'].browse(default_loc).name
|
||||
|
||||
picking_id = fields.Many2one(
|
||||
'stock.picking', 'Stock Picking',
|
||||
required=True,
|
||||
help='The stock operation where the packing has been made')
|
||||
product_id = fields.Many2one('product.product', 'Product', ondelete="cascade")
|
||||
product_uom_id = fields.Many2one('product.uom', 'Unit of Measure')
|
||||
product_qty = fields.Float('To Do', default=0.0, digits_compute=dp.get_precision('Product Unit of Measure'), required=True)
|
||||
ordered_qty = fields.Float('Ordered Quantity', digits_compute=dp.get_precision('Product Unit of Measure'))
|
||||
qty_done = fields.Float('Done', default=0.0, digits_compute=dp.get_precision('Product Unit of Measure'))
|
||||
# TDE FIXME: what what what what ??
|
||||
is_done = fields.Boolean(compute='_compute_is_done', inverse='_set_is_done', string='Done', oldname='processed_boolean')
|
||||
package_id = fields.Many2one('stock.quant.package', 'Source Package')
|
||||
pack_lot_ids = fields.One2many('stock.pack.operation.lot', 'operation_id', 'Lots Used')
|
||||
result_package_id = fields.Many2one(
|
||||
'stock.quant.package', 'Destination Package',
|
||||
ondelete='cascade', required=False,
|
||||
help="If set, the operations are packed into this package")
|
||||
date = fields.Datetime('Date', default=fields.Date.context_today, required=True)
|
||||
owner_id = fields.Many2one('res.partner', 'Owner', help="Owner of the quants")
|
||||
linked_move_operation_ids = fields.One2many(
|
||||
'stock.move.operation.link', 'operation_id', string='Linked Moves',
|
||||
readonly=True,
|
||||
help='Moves impacted by this operation for the computation of the remaining quantities')
|
||||
remaining_qty = fields.Float(
|
||||
compute='_get_remaining_qty', string="Remaining Qty", digits=0,
|
||||
help="Remaining quantity in default UoM according to moves matched with this operation.")
|
||||
location_id = fields.Many2one('stock.location', 'Source Location', required=True)
|
||||
location_dest_id = fields.Many2one('stock.location', 'Destination Location', required=True)
|
||||
picking_source_location_id = fields.Many2one('stock.location', related='picking_id.location_id')
|
||||
picking_destination_location_id = fields.Many2one('stock.location', related='picking_id.location_dest_id')
|
||||
# TDE FIXME: unnecessary fields IMO, to remove
|
||||
from_loc = fields.Char(compute='_compute_location_description', default=_get_default_from_loc, string='From')
|
||||
to_loc = fields.Char(compute='_compute_location_description', default=_get_default_to_loc, string='To')
|
||||
fresh_record = fields.Boolean('Newly created pack operation', default=True)
|
||||
lots_visible = fields.Boolean(compute='_compute_lots_visible')
|
||||
state = fields.Selection(selection=[
|
||||
('draft', 'Draft'),
|
||||
('cancel', 'Cancelled'),
|
||||
('waiting', 'Waiting Another Operation'),
|
||||
('confirmed', 'Waiting Availability'),
|
||||
('partially_available', 'Partially Available'),
|
||||
('assigned', 'Available'),
|
||||
('done', 'Done')], related='picking_id.state')
|
||||
|
||||
@api.one
|
||||
def _compute_is_done(self):
|
||||
self.is_done = self.qty_done > 0.0
|
||||
|
||||
@api.one
|
||||
def _set_is_done(self):
|
||||
# TDE FIXME: whuuuut ???
|
||||
if not self.product_id:
|
||||
if self.processed_boolean and self.qty_done == 0:
|
||||
self.qty_done = 1.0
|
||||
if not self.processed_boolean and self.qty_done != 0:
|
||||
self.qty_done = 0.0
|
||||
|
||||
def _get_remaining_prod_quantities(self):
|
||||
'''Get the remaining quantities per product on an operation with a package. This function returns a dictionary'''
|
||||
operation = self.browse(cr, uid, ids[0], context=context)
|
||||
#if the operation doesn't concern a package, it's not relevant to call this function
|
||||
if not operation.package_id or operation.product_id:
|
||||
return {operation.product_id: operation.remaining_qty}
|
||||
#get the total of products the package contains
|
||||
res = self.pool.get('stock.quant.package')._get_all_products_quantities(cr, uid, [operation.package_id.id], context=context)
|
||||
#reduce by the quantities linked to a move
|
||||
for record in operation.linked_move_operation_ids:
|
||||
# TDE CLEANME: merge with _get_all_products_quantities in quant to ease code understanding + clean code
|
||||
# if the operation doesn't concern a package, it's not relevant to call this function
|
||||
if not self.package_id or self.product_id:
|
||||
return {self.product_id: self.remaining_qty}
|
||||
# get the total of products the package contains
|
||||
res = self.package_id._get_all_products_quantities()
|
||||
# reduce by the quantities linked to a move
|
||||
for record in self.linked_move_operation_ids:
|
||||
if record.move_id.product_id.id not in res:
|
||||
res[record.move_id.product_id] = 0
|
||||
res[record.move_id.product_id] -= record.qty
|
||||
return res
|
||||
|
||||
def _get_remaining_qty(self, cr, uid, ids, name, args, context=None):
|
||||
uom_obj = self.pool.get('product.uom')
|
||||
res = {}
|
||||
for ops in self.browse(cr, uid, ids, context=context):
|
||||
res[ops.id] = 0
|
||||
if ops.package_id and not ops.product_id:
|
||||
#dont try to compute the remaining quantity for packages because it's not relevant (a package could include different products).
|
||||
#should use _get_remaining_prod_quantities instead
|
||||
continue
|
||||
else:
|
||||
qty = ops.product_qty
|
||||
if ops.product_uom_id:
|
||||
qty = uom_obj._compute_qty_obj(cr, uid, ops.product_uom_id, ops.product_qty, ops.product_id.uom_id, context=context)
|
||||
for record in ops.linked_move_operation_ids:
|
||||
qty -= record.qty
|
||||
res[ops.id] = float_round(qty, precision_rounding=ops.product_id.uom_id.rounding)
|
||||
return res
|
||||
|
||||
def product_id_change(self, cr, uid, ids, product_id, product_uom_id, product_qty, context=None):
|
||||
res = self.on_change_tests(cr, uid, ids, product_id, product_uom_id, product_qty, context=context)
|
||||
uom_obj = self.pool['product.uom']
|
||||
product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
|
||||
if product_id and not product_uom_id or uom_obj.browse(cr, uid, product_uom_id, context=context).category_id.id != product.uom_id.category_id.id:
|
||||
res['value']['product_uom_id'] = product.uom_id.id
|
||||
if product:
|
||||
res['value']['lots_visible'] = (product.tracking != 'none')
|
||||
res['domain'] = {'product_uom_id': [('category_id','=',product.uom_id.category_id.id)]}
|
||||
@api.one
|
||||
def _get_remaining_qty(self):
|
||||
if self.package_id and not self.product_id:
|
||||
# dont try to compute the remaining quantity for packages because it's not relevant (a package could include different products).
|
||||
# should use _get_remaining_prod_quantities instead
|
||||
# TDE FIXME: actually resolve the comment hereabove
|
||||
self.remaining_qty = 0
|
||||
else:
|
||||
res['domain'] = {'product_uom_id': []}
|
||||
return res
|
||||
qty = self.product_qty
|
||||
if self.product_uom_id:
|
||||
qty = self.env['product.uom']._compute_qty_obj(self.product_uom_id, self.product_qty, self.product_id.uom_id)
|
||||
for record in self.linked_move_operation_ids:
|
||||
qty -= record.qty
|
||||
self.remaining_qty = float_round(qty, precision_rounding=self.product_id.uom_id.rounding)
|
||||
|
||||
def on_change_tests(self, cr, uid, ids, product_id, product_uom_id, product_qty, context=None):
|
||||
res = {'value': {}}
|
||||
uom_obj = self.pool.get('product.uom')
|
||||
if product_id:
|
||||
product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
|
||||
product_uom_id = product_uom_id or product.uom_id.id
|
||||
selected_uom = uom_obj.browse(cr, uid, product_uom_id, context=context)
|
||||
if selected_uom.category_id.id != product.uom_id.category_id.id:
|
||||
res['warning'] = {
|
||||
'title': _('Warning: wrong UoM!'),
|
||||
'message': _('The selected UoM for product %s is not compatible with the UoM set on the product form. \nPlease choose an UoM within the same UoM category.') % (product.name)
|
||||
}
|
||||
if product_qty and 'warning' not in res:
|
||||
rounded_qty = uom_obj._compute_qty(cr, uid, product_uom_id, product_qty, product_uom_id, round=True)
|
||||
if rounded_qty != product_qty:
|
||||
res['warning'] = {
|
||||
'title': _('Warning: wrong quantity!'),
|
||||
'message': _('The chosen quantity for product %s is not compatible with the UoM rounding. It will be automatically converted at confirmation') % (product.name)
|
||||
}
|
||||
return res
|
||||
@api.one
|
||||
def _compute_location_description(self):
|
||||
self.from_loc = '%s%s' % (self.location_id.name, self.product_id and self.package_id.name or '')
|
||||
self.to_loc = '%s%s' % (self.location_dest_id.name, self.result_package_id.name or '')
|
||||
|
||||
def _compute_location_description(self, cr, uid, ids, field_name, arg, context=None):
|
||||
res = {}
|
||||
for op in self.browse(cr, uid, ids, context=context):
|
||||
from_name = op.location_id.name
|
||||
to_name = op.location_dest_id.name
|
||||
if op.package_id and op.product_id:
|
||||
from_name += " : " + op.package_id.name
|
||||
if op.result_package_id:
|
||||
to_name += " : " + op.result_package_id.name
|
||||
res[op.id] = {'from_loc': from_name,
|
||||
'to_loc': to_name}
|
||||
return res
|
||||
|
||||
def _compute_is_done(self, cr, uid, ids, field_name, arg, context=None):
|
||||
res = {}
|
||||
for pack in self.browse(cr, uid, ids, context=context):
|
||||
res[pack.id] = (pack.qty_done > 0.0)
|
||||
return res
|
||||
_get_bool = _compute_is_done
|
||||
|
||||
def _set_processed_qty(self, cr, uid, id, field_name, field_value, arg, context=None):
|
||||
op = self.browse(cr, uid, id, context=context)
|
||||
if not op.product_id:
|
||||
if field_value and op.qty_done == 0:
|
||||
self.write(cr, uid, [id], {'qty_done': 1.0}, context=context)
|
||||
if not field_value and op.qty_done != 0:
|
||||
self.write(cr, uid, [id], {'qty_done': 0.0}, context=context)
|
||||
return True
|
||||
|
||||
def _compute_lots_visible(self, cr, uid, ids, field_name, arg, context=None):
|
||||
res = {}
|
||||
for pack in self.browse(cr, uid, ids, context=context):
|
||||
if pack.pack_lot_ids:
|
||||
res[pack.id] = True
|
||||
continue
|
||||
pick = pack.picking_id
|
||||
product_requires = (pack.product_id.tracking != 'none')
|
||||
if pick.picking_type_id:
|
||||
res[pack.id] = (pick.picking_type_id.use_existing_lots or pick.picking_type_id.use_create_lots) and product_requires
|
||||
else:
|
||||
res[pack.id] = product_requires
|
||||
return res
|
||||
|
||||
def _get_default_from_loc(self, cr, uid, context=None):
|
||||
default_loc = context.get('default_location_id')
|
||||
if default_loc:
|
||||
return self.pool['stock.location'].browse(cr, uid, default_loc, context=context).name
|
||||
|
||||
def _get_default_to_loc(self, cr, uid, context=None):
|
||||
default_loc = context.get('default_location_dest_id')
|
||||
if default_loc:
|
||||
return self.pool['stock.location'].browse(cr, uid, default_loc, context=context).name
|
||||
|
||||
_columns = {
|
||||
'picking_id': fields.many2one('stock.picking', 'Stock Picking', help='The stock operation where the packing has been made', required=True),
|
||||
'product_id': fields.many2one('product.product', 'Product', ondelete="CASCADE"), # 1
|
||||
'product_uom_id': fields.many2one('product.uom', 'Unit of Measure'),
|
||||
'product_qty': fields.float('To Do', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
|
||||
'qty_done': fields.float('Done', digits_compute=dp.get_precision('Product Unit of Measure')),
|
||||
'is_done': fields.function(_compute_is_done, fnct_inv=_set_processed_qty, type='boolean', string='Done', oldname='processed_boolean'),
|
||||
'package_id': fields.many2one('stock.quant.package', 'Source Package'), # 2
|
||||
'pack_lot_ids': fields.one2many('stock.pack.operation.lot', 'operation_id', 'Lots Used'),
|
||||
'result_package_id': fields.many2one('stock.quant.package', 'Destination Package', help="If set, the operations are packed into this package", required=False, ondelete='cascade'),
|
||||
'date': fields.datetime('Date', required=True),
|
||||
'owner_id': fields.many2one('res.partner', 'Owner', help="Owner of the quants"),
|
||||
'linked_move_operation_ids': fields.one2many('stock.move.operation.link', 'operation_id', string='Linked Moves', readonly=True, help='Moves impacted by this operation for the computation of the remaining quantities'),
|
||||
'remaining_qty': fields.function(_get_remaining_qty, type='float', digits = 0, string="Remaining Qty", help="Remaining quantity in default UoM according to moves matched with this operation. "),
|
||||
'location_id': fields.many2one('stock.location', 'Source Location', required=True),
|
||||
'location_dest_id': fields.many2one('stock.location', 'Destination Location', required=True),
|
||||
'picking_source_location_id': fields.related('picking_id', 'location_id', type='many2one', relation='stock.location'),
|
||||
'picking_destination_location_id': fields.related('picking_id', 'location_dest_id', type='many2one', relation='stock.location'),
|
||||
'from_loc': fields.function(_compute_location_description, type='char', string='From', multi='loc'),
|
||||
'to_loc': fields.function(_compute_location_description, type='char', string='To', multi='loc'),
|
||||
'fresh_record': fields.boolean('Newly created pack operation'),
|
||||
'lots_visible': fields.function(_compute_lots_visible, type='boolean'),
|
||||
'state': fields.related('picking_id', 'state', type='selection', selection=[
|
||||
('draft', 'Draft'),
|
||||
('cancel', 'Cancelled'),
|
||||
('waiting', 'Waiting Another Operation'),
|
||||
('confirmed', 'Waiting Availability'),
|
||||
('partially_available', 'Partially Available'),
|
||||
('assigned', 'Available'),
|
||||
('done', 'Done'),
|
||||
]),
|
||||
'ordered_qty': fields.float('Ordered Quantity', digits_compute=dp.get_precision('Product Unit of Measure')),
|
||||
}
|
||||
|
||||
_defaults = {
|
||||
'date': fields.date.context_today,
|
||||
'qty_done': 0.0,
|
||||
'product_qty': 0.0,
|
||||
'processed_boolean': lambda *a: False,
|
||||
'fresh_record': True,
|
||||
'from_loc': _get_default_from_loc,
|
||||
'to_loc': _get_default_to_loc,
|
||||
}
|
||||
|
||||
def split_quantities(self, cr, uid, ids, context=None):
|
||||
for pack in self.browse(cr, uid, ids, context=context):
|
||||
if pack.product_qty - pack.qty_done > 0.0 and pack.qty_done < pack.product_qty:
|
||||
pack2 = self.copy(cr, uid, pack.id, default={'qty_done': 0.0, 'product_qty': pack.product_qty - pack.qty_done}, context=context)
|
||||
self.write(cr, uid, [pack.id], {'product_qty': pack.qty_done}, context=context)
|
||||
else:
|
||||
raise UserError(_('The quantity to split should be smaller than the quantity To Do. '))
|
||||
return True
|
||||
|
||||
def create(self, cr, uid, vals, context=None):
|
||||
vals['ordered_qty'] = vals.get('product_qty')
|
||||
return super(stock_pack_operation, self).create(cr, uid, vals, context=context)
|
||||
|
||||
def write(self, cr, uid, ids, vals, context=None):
|
||||
vals['fresh_record'] = False
|
||||
context = context or {}
|
||||
res = super(stock_pack_operation, self).write(cr, uid, ids, vals, context=context)
|
||||
return res
|
||||
|
||||
def unlink(self, cr, uid, ids, context=None):
|
||||
if any([x.state in ('done', 'cancel') for x in self.browse(cr, uid, ids, context=context)]):
|
||||
raise UserError(_('You can not delete pack operations of a done picking'))
|
||||
return super(stock_pack_operation, self).unlink(cr, uid, ids, context=context)
|
||||
|
||||
def check_tracking(self, cr, uid, ids, context=None):
|
||||
""" Checks if serial number is assigned to stock move or not and raise an error if it had to.
|
||||
"""
|
||||
operations = self.browse(cr, uid, ids, context=context)
|
||||
for ops in operations:
|
||||
if ops.picking_id and (ops.picking_id.picking_type_id.use_existing_lots or ops.picking_id.picking_type_id.use_create_lots) and \
|
||||
ops.product_id and ops.product_id.tracking != 'none' and ops.qty_done > 0.0:
|
||||
if not ops.pack_lot_ids:
|
||||
raise UserError(_('You need to provide a Lot/Serial Number for product %s') % ops.product_id.name)
|
||||
if ops.product_id.tracking == 'serial':
|
||||
for opslot in ops.pack_lot_ids:
|
||||
if opslot.qty not in (1.0, 0.0):
|
||||
raise UserError(_('You should provide a different serial number for each piece'))
|
||||
|
||||
def save(self, cr, uid, ids, context=None):
|
||||
for pack in self.browse(cr, uid, ids, context=context):
|
||||
if pack.product_id.tracking != 'none':
|
||||
qty_done = sum([x.qty for x in pack.pack_lot_ids])
|
||||
self.pool['stock.pack.operation'].write(cr, uid, [pack.id], {'qty_done': qty_done}, context=context)
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
||||
def action_split_lots(self, cr, uid, ids, context=None):
|
||||
context = context or {}
|
||||
ctx=context.copy()
|
||||
assert len(ids) > 0
|
||||
data_obj = self.pool['ir.model.data']
|
||||
pack = self.browse(cr, uid, ids[0], context=context)
|
||||
picking_type = pack.picking_id.picking_type_id
|
||||
serial = (pack.product_id.tracking == 'serial')
|
||||
view = data_obj.xmlid_to_res_id(cr, uid, 'stock.view_pack_operation_lot_form')
|
||||
only_create = picking_type.use_create_lots and not picking_type.use_existing_lots
|
||||
show_reserved = any([x for x in pack.pack_lot_ids if x.qty_todo > 0.0])
|
||||
ctx.update({'serial': serial,
|
||||
'only_create': only_create,
|
||||
'create_lots': picking_type.use_create_lots,
|
||||
'state_done': pack.picking_id.state == 'done',
|
||||
'show_reserved': show_reserved})
|
||||
return {
|
||||
'name': _('Lot Details'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_type': 'form',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'stock.pack.operation',
|
||||
'views': [(view, 'form')],
|
||||
'view_id': view,
|
||||
'target': 'new',
|
||||
'res_id': pack.id,
|
||||
'context': ctx,
|
||||
}
|
||||
split_lot = action_split_lots
|
||||
|
||||
def show_details(self, cr, uid, ids, context=None):
|
||||
data_obj = self.pool['ir.model.data']
|
||||
view = data_obj.xmlid_to_res_id(cr, uid, 'stock.view_pack_operation_details_form_save')
|
||||
pack = self.browse(cr, uid, ids[0], context=context)
|
||||
return {
|
||||
'name': _('Operation Details'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_type': 'form',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'stock.pack.operation',
|
||||
'views': [(view, 'form')],
|
||||
'view_id': view,
|
||||
'target': 'new',
|
||||
'res_id': pack.id,
|
||||
'context': context,
|
||||
}
|
||||
|
||||
|
||||
class stock_pack_operation_lot(osv.osv):
|
||||
_name = "stock.pack.operation.lot"
|
||||
_description = "Specifies lot/serial number for pack operations that need it"
|
||||
|
||||
def _get_plus(self, cr, uid, ids, field_name, arg, context=None):
|
||||
res = {}
|
||||
for packlot in self.browse(cr, uid, ids, context=context):
|
||||
if packlot.operation_id.product_id.tracking == 'serial':
|
||||
res[packlot.id] = (packlot.qty == 0.0)
|
||||
else:
|
||||
res[packlot.id] = (packlot.qty_todo == 0.0) or (packlot.qty < packlot.qty_todo)
|
||||
return res
|
||||
|
||||
_columns = {
|
||||
'operation_id': fields.many2one('stock.pack.operation'),
|
||||
'qty': fields.float('Done'),
|
||||
'lot_id': fields.many2one('stock.production.lot', 'Lot/Serial Number'),
|
||||
'lot_name': fields.char('Lot Name'),
|
||||
'qty_todo': fields.float('To Do'),
|
||||
'plus_visible': fields.function(_get_plus, type='boolean'),
|
||||
}
|
||||
|
||||
_defaults = {
|
||||
'qty': lambda cr, uid, ids, c: 1.0,
|
||||
'qty_todo': lambda cr, uid, ids, c: 0.0,
|
||||
'plus_visible': True,
|
||||
}
|
||||
|
||||
def _check_lot(self, cr, uid, ids, context=None):
|
||||
for packlot in self.browse(cr, uid, ids, context=context):
|
||||
if not packlot.lot_name and not packlot.lot_id:
|
||||
return False
|
||||
return True
|
||||
|
||||
_constraints = [
|
||||
(_check_lot,
|
||||
'Lot is required',
|
||||
['lot_id', 'lot_name']),
|
||||
]
|
||||
|
||||
_sql_constraints = [
|
||||
('qty', 'CHECK(qty >= 0.0)','Quantity must be greater than or equal to 0.0!'),
|
||||
('uniq_lot_id', 'unique(operation_id, lot_id)', 'You have already mentioned this lot in another line'),
|
||||
('uniq_lot_name', 'unique(operation_id, lot_name)', 'You have already mentioned this lot name in another line')]
|
||||
|
||||
def action_add_quantity(self, cr, uid, ids, context=None):
|
||||
pack_ids = []
|
||||
for packlot in self.browse(cr, uid, ids, context=context):
|
||||
self.write(cr, uid, [packlot.id], {'qty': packlot.qty + 1}, context=context)
|
||||
pack = packlot.operation_id
|
||||
pack_ids = [pack.id]
|
||||
qty_done = sum([x.qty for x in pack.pack_lot_ids])
|
||||
pack.write({'qty_done': qty_done})
|
||||
return self.pool['stock.pack.operation'].action_split_lots(cr, uid, pack_ids, context=context)
|
||||
do_plus = action_add_quantity
|
||||
|
||||
def action_remove_quantity(self, cr, uid, ids, context=None):
|
||||
pack_ids = []
|
||||
for packlot in self.browse(cr, uid, ids, context=context):
|
||||
self.write(cr, uid, [packlot.id], {'qty': packlot.qty - 1}, context=context)
|
||||
pack = packlot.operation_id
|
||||
pack_ids = [pack.id]
|
||||
qty_done = sum([x.qty for x in pack.pack_lot_ids])
|
||||
pack.write({'qty_done': qty_done})
|
||||
return self.pool['stock.pack.operation'].action_split_lots(cr, uid, pack_ids, context=context)
|
||||
do_minus = action_remove_quantity
|
||||
|
||||
|
||||
class stock_move_operation_link(osv.osv):
|
||||
"""
|
||||
Table making the link between stock.moves and stock.pack.operations to compute the remaining quantities on each of these objects
|
||||
"""
|
||||
_name = "stock.move.operation.link"
|
||||
_description = "Link between stock moves and pack operations"
|
||||
|
||||
_columns = {
|
||||
'qty': fields.float('Quantity', help="Quantity of products to consider when talking about the contribution of this pack operation towards the remaining quantity of the move (and inverse). Given in the product main uom."),
|
||||
'operation_id': fields.many2one('stock.pack.operation', 'Operation', required=True, ondelete="cascade"),
|
||||
'move_id': fields.many2one('stock.move', 'Move', required=True, ondelete="cascade"),
|
||||
'reserved_quant_id': fields.many2one('stock.quant', 'Reserved Quant', help="Technical field containing the quant that created this link between an operation and a stock move. Used at the stock_move_obj.action_done() time to avoid seeking a matching quant again"),
|
||||
}
|
||||
|
||||
|
||||
class StockPackOperation(models.Model):
|
||||
_inherit = 'stock.pack.operation'
|
||||
@api.one
|
||||
def _compute_lots_visible(self):
|
||||
if self.pack_lot_ids:
|
||||
self.lots_visible = True
|
||||
elif self.picking_id.picking_type_id and self.product_id.tracking != 'none': # TDE FIXME: not sure correctly migrated
|
||||
picking = self.picking_id
|
||||
self.lots_visible = picking.picking_type_id.use_existing_lots or picking.picking_type_id.use_create_lots
|
||||
else:
|
||||
self.lots_visible = self.product_id.tracking != 'none'
|
||||
|
||||
@api.onchange('pack_lot_ids')
|
||||
def _onchange_packlots(self):
|
||||
self.qty_done = sum([x.qty for x in self.pack_lot_ids])
|
||||
|
||||
@api.multi
|
||||
@api.onchange('product_id', 'product_uom_id')
|
||||
def onchange_product_id(self):
|
||||
if self.product_id:
|
||||
self.lots_visible = self.product_id.tracking != 'none'
|
||||
if not self.product_uom_id or self.product_uom_id.category_id != self.product_id.uom_id.category_id:
|
||||
self.product_uom_id = self.product_id.uom_id.id
|
||||
res = {'domain': {'product_uom_id': [('category_id', '=', self.product_uom_id.category_id.id)]}}
|
||||
else:
|
||||
res = {'domain': {'product_uom_id': []}}
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
vals['ordered_qty'] = vals.get('product_qty')
|
||||
return super(PackOperation, self).create(vals)
|
||||
|
||||
@api.multi
|
||||
def write(self, values):
|
||||
# TDE FIXME: weird stuff, protectin pack op ?
|
||||
values['fresh_record'] = False
|
||||
return super(PackOperation, self).write(values)
|
||||
|
||||
@api.multi
|
||||
def unlink(self):
|
||||
if any([operation.state in ('done', 'cancel') for operation in self]):
|
||||
raise UserError(_('You can not delete pack operations of a done picking'))
|
||||
return super(PackOperation, self).unlink()
|
||||
|
||||
@api.multi
|
||||
def split_quantities(self):
|
||||
for operation in self:
|
||||
if float_compare(operation.product_qty, operation.qty_done, precision_rounding=operation.product_uom_id.rounding) == 1:
|
||||
operation.copy(default={'qty_done': 0.0, 'product_qty': operation.product_qty - operation.qty_done})
|
||||
operation.write({'product_qty': operation.qty_done})
|
||||
else:
|
||||
raise UserError(_('The quantity to split should be smaller than the quantity To Do. '))
|
||||
return True
|
||||
|
||||
@api.multi
|
||||
def save(self):
|
||||
# TDE FIXME: does not seem to be used -> actually, it does
|
||||
# TDE FIXME: move me somewhere else, because the return indicated a wizard, in pack op, it is quite strange
|
||||
# HINT: 4. How to manage lots of identical products?
|
||||
# Create a picking and click on the Mark as TODO button to display the Lot Split icon. A window will pop-up. Click on Add an item and fill in the serial numbers and click on save button
|
||||
for pack in self:
|
||||
if pack.product_id.tracking != 'none':
|
||||
pack.write({'qty_done': sum(pack.pack_lot_ids.mapped('qty'))})
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
||||
@api.multi
|
||||
def action_split_lots(self):
|
||||
action_ctx = dict(self.env.context)
|
||||
picking_type = self.picking_id.picking_type_id
|
||||
action_ctx.update({
|
||||
'serial': self.product_id.tracking == 'serial',
|
||||
'only_create': picking_type.use_create_lots and not picking_type.use_existing_lots,
|
||||
'create_lots': picking_type.use_create_lots,
|
||||
'state_done': self.picking_id.state == 'done',
|
||||
'show_reserved': any([lot for lot in self.pack_lot_ids if lot.qty_todo > 0.0])})
|
||||
view_id = self.env.ref('stock.view_pack_operation_lot_form').id
|
||||
return {
|
||||
'name': _('Lot Details'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_type': 'form',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'stock.pack.operation',
|
||||
'views': [(view_id, 'form')],
|
||||
'view_id': view_id,
|
||||
'target': 'new',
|
||||
'res_id': self.ids[0],
|
||||
'context': action_ctx}
|
||||
split_lot = action_split_lots
|
||||
|
||||
@api.multi
|
||||
def show_details(self):
|
||||
# TDE FIXME: does not seem to be used
|
||||
view_id = self.env.ref('stock.view_pack_operation_details_form_save').id
|
||||
return {
|
||||
'name': _('Operation Details'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_type': 'form',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'stock.pack.operation',
|
||||
'views': [(view_id, 'form')],
|
||||
'view_id': view_id,
|
||||
'target': 'new',
|
||||
'res_id': self.ids[0],
|
||||
'context': self.env.context}
|
||||
|
||||
@api.multi
|
||||
def _check_serial_number(self):
|
||||
for operation in self:
|
||||
if operation.picking_id and \
|
||||
(operation.picking_id.picking_type_id.use_existing_lots or operation.picking_id.picking_type_id.use_create_lots) and \
|
||||
operation.product_id and operation.product_id.tracking != 'none' and \
|
||||
operation.qty_done > 0.0:
|
||||
if not operation.pack_lot_ids:
|
||||
raise UserError(_('You need to provide a Lot/Serial Number for product %s') % operation.product_id.name)
|
||||
if operation.product_id.tracking == 'serial':
|
||||
for opslot in operation.pack_lot_ids:
|
||||
if opslot.qty not in (1.0, 0.0):
|
||||
raise UserError(_('You should provide a different serial number for each piece'))
|
||||
check_tracking = _check_serial_number
|
||||
|
||||
|
||||
class PackOperationLot(models.Model):
|
||||
_name = "stock.pack.operation.lot"
|
||||
_description = "Lot / Serial number for pack ops"
|
||||
|
||||
operation_id = fields.Many2one('stock.pack.operation')
|
||||
qty = fields.Float('Done', default=1.0)
|
||||
lot_id = fields.Many2one('stock.production.lot', 'Lot/Serial Number')
|
||||
lot_name = fields.Char('Lot Name')
|
||||
qty_todo = fields.Float('To Do', default=0.0)
|
||||
plus_visible = fields.Boolean(compute='_compute_plus_visible', default=True)
|
||||
|
||||
_sql_constraints = [
|
||||
('qty', 'CHECK(qty >= 0.0)', 'Quantity must be greater than or equal to 0.0!'),
|
||||
('uniq_lot_id', 'unique(operation_id, lot_id)', 'You have already mentioned this lot in another line'),
|
||||
('uniq_lot_name', 'unique(operation_id, lot_name)', 'You have already mentioned this lot name in another line')]
|
||||
|
||||
@api.one
|
||||
def _compute_plus_visible(self):
|
||||
if self.operation_id.product_id.tracking == 'serial':
|
||||
self.plus_visible = (self.qty == 0.0)
|
||||
else:
|
||||
self.plus_visible = (self.qty_todo == 0.0) or (self.qty < self.qty_todo)
|
||||
|
||||
@api.constrains('lot_id', 'lot_name')
|
||||
def _check_lot(self):
|
||||
if any(not lot.lot_name and not lot.lot_id for lot in self):
|
||||
raise ValidationError(_('Lot required'))
|
||||
return True
|
||||
|
||||
def action_add_quantity(self, quantity):
|
||||
for lot in self:
|
||||
lot.write({'qty': lot.qty + quantity})
|
||||
lot.operation_id.write({'qty_done': sum(operation_lot.qty for operation_lot in lot.operation_id.pack_lot_ids)})
|
||||
return self.mapped('operation_id').action_split_lots()
|
||||
|
||||
@api.multi
|
||||
def do_plus(self):
|
||||
return self.action_add_quantity(1)
|
||||
|
||||
@api.multi
|
||||
def do_minus(self):
|
||||
return self.action_add_quantity(-1)
|
||||
|
||||
|
||||
class OperationLink(models.Model):
|
||||
""" Make link between stock.move and stock.pack.operation in order to compute
|
||||
the remaining quantities on each of those objects. """
|
||||
_name = "stock.move.operation.link"
|
||||
_description = "Pack Operation / Moves Link"
|
||||
|
||||
qty = fields.Float(
|
||||
'Quantity', help="Quantity of products to consider when talking about the contribution of this pack operation towards the "
|
||||
"remaining quantity of the move (and inverse). Given in the product main uom.")
|
||||
operation_id = fields.Many2one(
|
||||
'stock.pack.operation', 'Operation',
|
||||
ondelete="cascade", required=True)
|
||||
move_id = fields.Many2one(
|
||||
'stock.move', 'Move',
|
||||
ondelete="cascade", required=True)
|
||||
reserved_quant_id = fields.Many2one(
|
||||
'stock.quant', 'Reserved Quant',
|
||||
help="Technical field containing the quant that created this link between an operation and a stock move. "
|
||||
"Used at the stock_move_obj.action_done() time to avoid seeking a matching quant again")
|
||||
|
||||
@@ -246,7 +246,7 @@
|
||||
<field name="pack_operation_product_ids" options="{'reload_on_button': True}" context="{'default_picking_id': id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id}" mode="tree,kanban">
|
||||
<tree editable="bottom" decoration-muted="result_package_id" decoration-danger="qty_done>product_qty" decoration-success="qty_done==product_qty and state!='done' and not result_package_id">
|
||||
<field name="package_id" groups="stock.group_tracking_lot" invisible="1"/>
|
||||
<field name="product_id" on_change="product_id_change(product_id, product_uom_id, product_qty)" required="1" attrs="{'readonly': [('fresh_record', '=', False)]}"/>
|
||||
<field name="product_id" required="1" attrs="{'readonly': [('fresh_record', '=', False)]}"/>
|
||||
<field name="fresh_record" invisible="1"/>
|
||||
<field name="product_uom_id" attrs="{'readonly': [('fresh_record', '=', False)]}" groups="product.group_uom"/>
|
||||
<field name="lots_visible" invisible="1"/>
|
||||
@@ -266,7 +266,7 @@
|
||||
</tree>
|
||||
<kanban class="o_kanban_mobile">
|
||||
<field name="package_id" groups="stock.group_tracking_lot" invisible="1"/>
|
||||
<field name="product_id" on_change="product_id_change(product_id, product_uom_id, product_qty)" required="1" attrs="{'readonly': [('fresh_record', '=', False)]}"/>
|
||||
<field name="product_id" required="1" attrs="{'readonly': [('fresh_record', '=', False)]}"/>
|
||||
<field name="fresh_record" invisible="1"/>
|
||||
<field name="product_uom_id" attrs="{'readonly': [('fresh_record', '=', False)]}" groups="product.group_uom"/>
|
||||
<field name="lots_visible" invisible="1"/>
|
||||
@@ -339,6 +339,8 @@
|
||||
<field name="product_uom_qty"/>
|
||||
<field name="product_uom" options="{'no_open': True, 'no_create': True}"/>
|
||||
<field name="location_dest_id"/>
|
||||
<field name="scrapped"/>
|
||||
<field name="state"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div t-attf-class="oe_kanban_card oe_kanban_global_click">
|
||||
|
||||
Reference in New Issue
Block a user