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.
303 lines
14 KiB
Python
303 lines
14 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
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
|
|
|
|
|
|
class PackOperation(models.Model):
|
|
_name = "stock.pack.operation"
|
|
_description = "Packing Operation"
|
|
_order = "result_package_id desc, id"
|
|
|
|
# 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'''
|
|
# 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
|
|
|
|
@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:
|
|
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)
|
|
|
|
@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 '')
|
|
|
|
@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")
|