diff --git a/addons/stock/__openerp__.py b/addons/stock/__openerp__.py index 53055cbcc05..0beeccad55c 100644 --- a/addons/stock/__openerp__.py +++ b/addons/stock/__openerp__.py @@ -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', diff --git a/addons/stock/models/__init__.py b/addons/stock/models/__init__.py index e59796bb985..eda29ad9633 100644 --- a/addons/stock/models/__init__.py +++ b/addons/stock/models/__init__.py @@ -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 diff --git a/addons/stock/models/stock_pack_operation.py b/addons/stock/models/stock_pack_operation.py new file mode 100644 index 00000000000..e0827f40dfe --- /dev/null +++ b/addons/stock/models/stock_pack_operation.py @@ -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]) diff --git a/addons/stock/stock.py b/addons/stock/stock.py index 4fed09628c3..080475648c0 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -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]) diff --git a/addons/stock/stock_view.xml b/addons/stock/stock_view.xml index b1a99d19011..e2116807990 100644 --- a/addons/stock/stock_view.xml +++ b/addons/stock/stock_view.xml @@ -295,122 +295,6 @@ Defaults and Internal Pickings --> - - stock.pack.operation.details.form - stock.pack.operation - - -
- - - - - - - - - - - - - - - - -
-
-
- - - stock.pack.operation.details.form.save - stock.pack.operation - primary - - - - -
-
-
- -