[MOV] stock: stock_pack_operation

Pack operation models and views are moved in their own file. Impacted:

 - stock.pack.operation
 - stock.pack.operation.lot
 - stock.move.operation.link

This commit only moves files. No change is done.
This commit is contained in:
Thibault Delavallée
2016-04-29 12:18:41 +02:00
parent 70e1d5d5bc
commit 65fdba526f
6 changed files with 485 additions and 471 deletions
+1
View File
@@ -77,6 +77,7 @@ Dashboard / Reports for Inventory Management will include:
'views/stock_quant_views.xml',
'views/procurement_views.xml',
'views/stock_warehouse_views.xml',
'views/stock_pack_operation_views.xml',
'views/product_views.xml',
'views/stock_config_settings_views.xml',
+1
View File
@@ -9,6 +9,7 @@ import res_company
import res_partner
import stock_config_settings
import stock_inventory
import stock_pack_operation
import stock_picking
import stock_quant
import stock_warehouse
+365
View File
@@ -0,0 +1,365 @@
# -*- 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 openerp.report import report_sxw
class stock_pack_operation(osv.osv):
_name = "stock.pack.operation"
_description = "Packing Operation"
_order = "result_package_id desc, id"
def _get_remaining_prod_quantities(self, cr, uid, ids, context=None):
'''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:
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)]}
else:
res['domain'] = {'product_uom_id': []}
return res
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
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.onchange('pack_lot_ids')
def _onchange_packlots(self):
self.qty_done = sum([x.qty for x in self.pack_lot_ids])
-355
View File
@@ -1495,358 +1495,3 @@ class stock_location_path(osv.osv):
'move_dest_id': move_id,
})
move_obj.action_confirm(cr, uid, [move_id], context=None)
from openerp.report import report_sxw
class stock_pack_operation(osv.osv):
_name = "stock.pack.operation"
_description = "Packing Operation"
_order = "result_package_id desc, id"
def _get_remaining_prod_quantities(self, cr, uid, ids, context=None):
'''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:
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)]}
else:
res['domain'] = {'product_uom_id': []}
return res
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
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.onchange('pack_lot_ids')
def _onchange_packlots(self):
self.qty_done = sum([x.qty for x in self.pack_lot_ids])
-116
View File
@@ -295,122 +295,6 @@
Defaults and Internal Pickings
-->
<record id="view_pack_operation_details_form" model="ir.ui.view">
<field name="name">stock.pack.operation.details.form</field>
<field name="model">stock.pack.operation</field>
<field eval="12" name="priority"/>
<field name="arch" type="xml">
<form>
<group name="qty_to">
<group string="Products">
<field name="lots_visible" invisible="1"/>
<field name="picking_source_location_id" invisible="1"/>
<field name="picking_destination_location_id" invisible="1"/>
<field name="product_id" attrs="{'required': [('package_id', '=', False)]}" readonly="1"/>
<label for="qty_done"/>
<div>
<field name="qty_done" nolabel="1" attrs="{'readonly': [('lots_visible','=',True)]}" class="oe_inline"/>
/
<field name="product_qty" class="oe_inline" readonly="1"/>
<field name="product_uom_id" options="{'no_open': True, 'no_create': True}" groups="product.group_uom" class="oe_inline"/>
</div>
</group>
<group string="Locations" groups="stock.group_stock_multi_locations">
<field name="location_id" domain="[('id', 'child_of', picking_source_location_id)]"/>
<field name="location_dest_id" domain="[('id', 'child_of', picking_destination_location_id)]"/>
</group>
<group string="Packages" groups="stock.group_tracking_lot">
<field name="package_id" attrs="{'required': [('product_id', '=', False)]}" context="{'location_id': location_id}"/>
<field name="result_package_id" context="{'location_id': location_dest_id}"/>
</group>
</group>
</form>
</field>
</record>
<record id="view_pack_operation_details_form_save" model="ir.ui.view">
<field name="name">stock.pack.operation.details.form.save</field>
<field name="model">stock.pack.operation</field>
<field name="mode">primary</field>
<field name="inherit_id" ref="stock.view_pack_operation_details_form"/>
<field eval="15" name="priority"/>
<field name="arch" type="xml">
<group name="qty_to" position="after">
<footer>
<button name="save" type="object" string="Save" class="oe_highlight"/>
<button string="Discard" special="cancel"/>
</footer>
</group>
<field name="product_uom_id" position="after">
<button name="split_quantities" attrs="{'invisible': ['|', ('qty_done', '&lt;=', 0.0), ('product_qty', '&lt;=', 0.0)]}" class="oe_link" string="Split" type="object"/>
</field>
</field>
</record>
<record id="pack_details" model="ir.actions.act_window">
<field name="name">Pack Details</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">stock.pack.operation</field>
<field name="view_id" ref="view_pack_operation_details_form_save"/>
<field name="view_type">form</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="context">{}</field>
</record>
<record id="view_pack_operation_lot_form" model="ir.ui.view">
<field name="name">stock.pack.operation.lots.form</field>
<field name="model">stock.pack.operation</field>
<field eval="20" name="priority"/>
<field name="arch" type="xml">
<form>
<group>
<field name="product_id" readonly="1"/>
<label for="qty_done"/>
<span>
<field name="qty_done" class="oe_inline" readonly="1"/> of
<field name="product_qty" class="oe_inline" readonly="1"/>
<field name="product_uom_id" class="oe_inline" groups="product.group_uom" readonly="1"/>
<field name="state" invisible="1"/>
</span>
</group>
<group>
<field name="pack_lot_ids" nolabel="1" attrs="{'readonly': [('state', '=', 'done')]}">
<tree editable="bottom" decoration-success="qty_todo==qty" decoration-danger="(qty_todo &gt; 0) and (qty&gt;qty_todo)">
<field name="lot_name" invisible="not context.get('only_create', False)"/>
<field name="lot_id" invisible="context.get('only_create', False)"
domain="[('product_id','=', parent.product_id)]"
context="{'product_id': parent.product_id}"/>
<field name="qty_todo" invisible="not context.get('show_reserved') or context.get('serial') or context.get('state_done')" readonly="1"/>
<field name="qty" invisible="context.get('serial')"/>
<button name="do_minus" type="object" icon="fa-minus-square" attrs="{'invisible': [('qty', '&lt;=', 0.99)]}" invisible="not context.get('show_reserved') or context.get('state_done')"/>
<button name="do_plus" type="object" icon="fa-plus-square" attrs="{'invisible': [('plus_visible', '=', False)]}" invisible="not context.get('show_reserved') or context.get('state_done')"/>
<field name="plus_visible" invisible="1"/>
</tree>
</field>
</group>
<footer>
<button name="save" type="object" string="Save" class="oe_highlight" attrs="{'invisible':[('state','=','done')]}"/>
<button string="Discard" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="view_pack_operation_lot_lot_form" model="ir.ui.view">
<field name="name">stock.pack.operation.lots.form</field>
<field name="model">stock.pack.operation.lot</field>
<field eval="20" name="priority"/>
<field name="arch" type="xml">
<form>
<group>
<field name="lot_id"/>
<field name="qty"/>
</group>
</form>
</field>
</record>
<record id="view_move_pivot" model="ir.ui.view">
<field name="name">stock.move.pivot</field>
<field name="model">stock.move</field>
@@ -0,0 +1,118 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_pack_operation_details_form" model="ir.ui.view">
<field name="name">stock.pack.operation.details.form</field>
<field name="model">stock.pack.operation</field>
<field eval="12" name="priority"/>
<field name="arch" type="xml">
<form>
<group name="qty_to">
<group string="Products">
<field name="lots_visible" invisible="1"/>
<field name="picking_source_location_id" invisible="1"/>
<field name="picking_destination_location_id" invisible="1"/>
<field name="product_id" attrs="{'required': [('package_id', '=', False)]}" readonly="1"/>
<label for="qty_done"/>
<div>
<field name="qty_done" nolabel="1" attrs="{'readonly': [('lots_visible','=',True)]}" class="oe_inline"/>
/
<field name="product_qty" class="oe_inline" readonly="1"/>
<field name="product_uom_id" options="{'no_open': True, 'no_create': True}" groups="product.group_uom" class="oe_inline"/>
</div>
</group>
<group string="Locations" groups="stock.group_stock_multi_locations">
<field name="location_id" domain="[('id', 'child_of', picking_source_location_id)]"/>
<field name="location_dest_id" domain="[('id', 'child_of', picking_destination_location_id)]"/>
</group>
<group string="Packages" groups="stock.group_tracking_lot">
<field name="package_id" attrs="{'required': [('product_id', '=', False)]}" context="{'location_id': location_id}"/>
<field name="result_package_id" context="{'location_id': location_dest_id}"/>
</group>
</group>
</form>
</field>
</record>
<record id="view_pack_operation_details_form_save" model="ir.ui.view">
<field name="name">stock.pack.operation.details.form.save</field>
<field name="model">stock.pack.operation</field>
<field name="mode">primary</field>
<field name="inherit_id" ref="stock.view_pack_operation_details_form"/>
<field eval="15" name="priority"/>
<field name="arch" type="xml">
<group name="qty_to" position="after">
<footer>
<button name="save" type="object" string="Save" class="oe_highlight"/>
<button string="Discard" special="cancel"/>
</footer>
</group>
<field name="product_uom_id" position="after">
<button name="split_quantities" attrs="{'invisible': ['|', ('qty_done', '&lt;=', 0.0), ('product_qty', '&lt;=', 0.0)]}" class="oe_link" string="Split" type="object"/>
</field>
</field>
</record>
<record id="pack_details" model="ir.actions.act_window">
<field name="name">Pack Details</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">stock.pack.operation</field>
<field name="view_id" ref="view_pack_operation_details_form_save"/>
<field name="view_type">form</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="context">{}</field>
</record>
<record id="view_pack_operation_lot_form" model="ir.ui.view">
<field name="name">stock.pack.operation.lots.form</field>
<field name="model">stock.pack.operation</field>
<field eval="20" name="priority"/>
<field name="arch" type="xml">
<form>
<group>
<field name="product_id" readonly="1"/>
<label for="qty_done"/>
<span>
<field name="qty_done" class="oe_inline" readonly="1"/> of
<field name="product_qty" class="oe_inline" readonly="1"/>
<field name="product_uom_id" class="oe_inline" groups="product.group_uom" readonly="1"/>
<field name="state" invisible="1"/>
</span>
</group>
<group>
<field name="pack_lot_ids" nolabel="1" attrs="{'readonly': [('state', '=', 'done')]}">
<tree editable="bottom" decoration-success="qty_todo==qty" decoration-danger="(qty_todo &gt; 0) and (qty&gt;qty_todo)">
<field name="lot_name" invisible="not context.get('only_create', False)"/>
<field name="lot_id" invisible="context.get('only_create', False)"
domain="[('product_id','=', parent.product_id)]"
context="{'product_id': parent.product_id}"/>
<field name="qty_todo" invisible="not context.get('show_reserved') or context.get('serial') or context.get('state_done')" readonly="1"/>
<field name="qty" invisible="context.get('serial')"/>
<button name="do_minus" type="object" icon="fa-minus-square" attrs="{'invisible': [('qty', '&lt;=', 0.99)]}" invisible="not context.get('show_reserved') or context.get('state_done')"/>
<button name="do_plus" type="object" icon="fa-plus-square" attrs="{'invisible': [('plus_visible', '=', False)]}" invisible="not context.get('show_reserved') or context.get('state_done')"/>
<field name="plus_visible" invisible="1"/>
</tree>
</field>
</group>
<footer>
<button name="save" type="object" string="Save" class="oe_highlight" attrs="{'invisible':[('state','=','done')]}"/>
<button string="Discard" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="view_pack_operation_lot_lot_form" model="ir.ui.view">
<field name="name">stock.pack.operation.lots.form</field>
<field name="model">stock.pack.operation.lot</field>
<field eval="20" name="priority"/>
<field name="arch" type="xml">
<form>
<group>
<field name="lot_id"/>
<field name="qty"/>
</group>
</form>
</field>
</record>
</odoo>