From e912961a2df78f5fe3a1c9d9bed9382713afb276 Mon Sep 17 00:00:00 2001 From: Suherdy Yacob Date: Mon, 29 Dec 2025 10:45:30 +0700 Subject: [PATCH] add feature to auto generate lot number on resupply subcontract receipt --- __init__.py | 3 +- __manifest__.py | 19 ++- models/__init__.py | 3 +- models/stock_move.py | 123 +++++++++++++- models/stock_move_line.py | 54 +++++- models/stock_picking.py | 166 ++++++++++++++++++ security/ir.model.access.csv | 2 + tests/test_subcontract_lots.py | 189 +++++++++++++++++++++ views/stock_move_views.xml | 20 +++ views/stock_picking_views.xml | 69 ++++++++ wizard/__init__.py | 1 + wizard/subcontract_lot_generator.py | 111 ++++++++++++ wizard/subcontract_lot_generator_views.xml | 41 +++++ 13 files changed, 791 insertions(+), 10 deletions(-) create mode 100644 models/stock_picking.py create mode 100644 security/ir.model.access.csv create mode 100644 tests/test_subcontract_lots.py create mode 100644 views/stock_move_views.xml create mode 100644 views/stock_picking_views.xml create mode 100644 wizard/__init__.py create mode 100644 wizard/subcontract_lot_generator.py create mode 100644 wizard/subcontract_lot_generator_views.xml diff --git a/__init__.py b/__init__.py index 9a7e03e..c536983 100644 --- a/__init__.py +++ b/__init__.py @@ -1 +1,2 @@ -from . import models \ No newline at end of file +from . import models +from . import wizard \ No newline at end of file diff --git a/__manifest__.py b/__manifest__.py index 5b11b12..f8c8634 100644 --- a/__manifest__.py +++ b/__manifest__.py @@ -1,6 +1,6 @@ { 'name': 'Product Lot Sequence Per Product', - 'version': '1.1.1', + 'version': '1.2.1', 'category': 'Inventory/Inventory', 'summary': 'Per-product lot/serial sequences with performance optimization for large batches', 'description': """ @@ -15,6 +15,8 @@ * 8-10x speedup for large batch operations * Support for receipts, manufacturing orders, and manual generation * Date format codes support (%(y)s, %(month)s, %(day)s, etc.) + * Automatic lot generation for subcontracting moves + * Auto-generate button (+ icon) for subcontract receipts Performance: ----------- @@ -23,20 +25,25 @@ * Automatic optimization for quantities > 10 units * Tested with up to 500,000 units - New in v1.1: + New in v1.2.1: ----------- - * Major performance improvements for large batches - * Date format code support in sequences - * Comprehensive test suites - * Detailed performance documentation + * Working automatic lot generation for subcontracting moves + * Clean UI with essential buttons only + * Robust field compatibility for Odoo 18 + * Enhanced error handling and user feedback """, 'author': 'Suherdy Yacob', 'depends': [ 'stock', 'mrp', + 'mrp_subcontracting', ], 'data': [ + 'security/ir.model.access.csv', 'views/product_views.xml', + 'wizard/subcontract_lot_generator_views.xml', + 'views/stock_picking_views.xml', + 'views/stock_move_views.xml', ], 'installable': True, 'auto_install': False, diff --git a/models/__init__.py b/models/__init__.py index e66ea8a..aab96c7 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -2,4 +2,5 @@ from . import product_template from . import stock_move from . import stock_lot from . import stock_move_line -from . import mrp_production \ No newline at end of file +from . import mrp_production +from . import stock_picking \ No newline at end of file diff --git a/models/stock_move.py b/models/stock_move.py index 24dfe75..a99ef50 100644 --- a/models/stock_move.py +++ b/models/stock_move.py @@ -174,4 +174,125 @@ class StockMove(models.Model): return vals_list # Fallback to standard behavior - return super().action_generate_lot_line_vals(context, mode, first_lot, count, lot_text) \ No newline at end of file + return super().action_generate_lot_line_vals(context, mode, first_lot, count, lot_text) + + def _auto_generate_lots_for_subcontract(self): + """ + Auto-generate lot numbers for subcontracting moves. + This method handles the automatic lot generation for subcontracted products. + """ + self.ensure_one() + + if not self.product_id.tracking in ['lot', 'serial']: + return [] + + product = self.product_id + tmpl = product.product_tmpl_id + lot_sequence = getattr(tmpl, 'lot_sequence_id', False) + + if not lot_sequence: + _logger.warning(f"No lot sequence configured for product {product.display_name}") + return [] + + # Check if we already have move lines with lots + existing_lots = self.move_line_ids.filtered(lambda ml: ml.lot_id) + if existing_lots: + _logger.info(f"Move already has lots assigned: {[lot.lot_id.name for lot in existing_lots]}") + return existing_lots.mapped('lot_id') + + # Calculate how many lots we need to generate + remaining_qty = self.product_uom_qty + + # For serial tracking, create one lot per unit + if product.tracking == 'serial': + lots_needed = int(remaining_qty) + else: + # For lot tracking, create one lot for the entire quantity + lots_needed = 1 + + if lots_needed <= 0: + return [] + + # Generate lot names using the optimized batch method + if lots_needed > 10: + lot_names = self._allocate_sequence_batch(lot_sequence, lots_needed) + else: + lot_names = [lot_sequence.next_by_id() for _ in range(lots_needed)] + + # Create the lots + Lot = self.env['stock.lot'] + lot_vals_list = [] + for lot_name in lot_names: + lot_vals = { + 'name': lot_name, + 'product_id': product.id, + 'company_id': self.company_id.id, + } + lot_vals_list.append(lot_vals) + + lots = Lot.create(lot_vals_list) + + # Assign lots to existing move lines or create new ones + existing_move_lines = self.move_line_ids.filtered(lambda ml: not ml.lot_id) + + if existing_move_lines and product.tracking == 'lot': + # For lot tracking, assign the first lot to the first available move line + if lots: + existing_move_lines[0].lot_id = lots[0].id + _logger.info(f"Assigned lot {lots[0].name} to existing move line") + elif product.tracking == 'serial': + # For serial tracking, we need one move line per lot + # If we don't have enough move lines, let Odoo handle the creation + # by using the standard lot assignment mechanism + for i, lot in enumerate(lots): + if i < len(existing_move_lines): + existing_move_lines[i].lot_id = lot.id + else: + # Let Odoo create additional move lines as needed + # This is safer than trying to create them manually + break + + _logger.info(f"Auto-generated {len(lots)} lots for subcontract move of product {product.display_name}") + return lots + + def action_generate_lots_for_move(self): + """Open the lot generator wizard for this specific move.""" + self.ensure_one() + + if not self.is_subcontract: + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': 'Not a Subcontract Move', + 'message': 'This action is only available for subcontract moves.', + 'type': 'warning', + 'sticky': False, + } + } + + if self.product_id.tracking == 'none': + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': 'No Tracking Required', + 'message': 'This product does not require lot/serial tracking.', + 'type': 'info', + 'sticky': False, + } + } + + return { + 'name': 'Generate Lots for Move', + 'type': 'ir.actions.act_window', + 'res_model': 'subcontract.lot.generator', + 'view_mode': 'form', + 'target': 'new', + 'context': { + 'default_move_id': self.id, + 'default_picking_id': self.picking_id.id, + 'default_product_id': self.product_id.id, + 'default_quantity': self.product_uom_qty, + }, + } \ No newline at end of file diff --git a/models/stock_move_line.py b/models/stock_move_line.py index 559b469..6f648c1 100644 --- a/models/stock_move_line.py +++ b/models/stock_move_line.py @@ -21,4 +21,56 @@ class StockMoveLine(models.Model): } if self.product_id.company_id and self.company_id in (self.product_id.company_id.all_child_ids | self.product_id.company_id): vals['company_id'] = self.company_id.id - return vals \ No newline at end of file + return vals + + def action_open_lot_generator(self): + """Open the lot generator wizard for this move line.""" + self.ensure_one() + + if not self.product_id: + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': 'No Product', + 'message': 'Please select a product first.', + 'type': 'warning', + 'sticky': False, + } + } + + if self.product_id.tracking == 'none': + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': 'No Tracking Required', + 'message': 'This product does not require lot/serial tracking.', + 'type': 'info', + 'sticky': False, + } + } + + # Use safe field access for quantity fields (field names may vary in different Odoo versions) + MoveLine = self.env['stock.move.line'] + quantity_value = 1.0 # default fallback + + # Try different quantity field names + for field_name in ['reserved_uom_qty', 'product_qty', 'product_uom_qty']: + if field_name in MoveLine._fields and hasattr(self, field_name): + quantity_value = getattr(self, field_name, 1.0) or 1.0 + break + + return { + 'name': 'Generate Lots for Move Line', + 'type': 'ir.actions.act_window', + 'res_model': 'subcontract.lot.generator', + 'view_mode': 'form', + 'target': 'new', + 'context': { + 'default_move_id': self.move_id.id, + 'default_picking_id': self.picking_id.id, + 'default_product_id': self.product_id.id, + 'default_quantity': quantity_value, + }, + } \ No newline at end of file diff --git a/models/stock_picking.py b/models/stock_picking.py new file mode 100644 index 0000000..e3b252a --- /dev/null +++ b/models/stock_picking.py @@ -0,0 +1,166 @@ +from odoo import api, fields, models +import logging + +_logger = logging.getLogger(__name__) + + +class StockPicking(models.Model): + _inherit = 'stock.picking' + + subcontract_lot_count = fields.Integer( + 'Subcontract Lots Count', + compute='_compute_subcontract_lot_count' + ) + has_subcontract_moves = fields.Boolean( + 'Has Subcontract Moves', + compute='_compute_has_subcontract_moves' + ) + + @api.depends('move_ids.is_subcontract') + def _compute_has_subcontract_moves(self): + """Compute if this picking has any subcontract moves.""" + for picking in self: + picking.has_subcontract_moves = any(move.is_subcontract for move in picking.move_ids) + + @api.depends('move_ids.move_line_ids.lot_id') + def _compute_subcontract_lot_count(self): + """Compute the number of lots generated for subcontract moves.""" + for picking in self: + if picking.picking_type_code == 'incoming': + subcontract_moves = picking.move_ids.filtered('is_subcontract') + lot_ids = subcontract_moves.move_line_ids.mapped('lot_id') + picking.subcontract_lot_count = len(lot_ids) + else: + picking.subcontract_lot_count = 0 + + def action_view_generated_lots(self): + """Action to view all lots generated for subcontract moves in this picking.""" + self.ensure_one() + subcontract_moves = self.move_ids.filtered('is_subcontract') + lot_ids = subcontract_moves.move_line_ids.mapped('lot_id').ids + + return { + 'name': 'Generated Lots', + 'type': 'ir.actions.act_window', + 'res_model': 'stock.lot', + 'view_mode': 'tree,form', + 'domain': [('id', 'in', lot_ids)], + 'context': {'create': False}, + } + + def action_auto_generate_lots_subcontract(self): + """ + Action to auto-generate lot numbers for subcontracting moves. + Similar to the "+" icon functionality in MO forms. + """ + generated_count = 0 + generated_lots = [] + + for picking in self: + if picking.picking_type_code == 'incoming': + subcontract_moves = picking.move_ids.filtered('is_subcontract') + if not subcontract_moves: + continue + for move in subcontract_moves: + if move.product_id.tracking in ['lot', 'serial']: + try: + lots = move._auto_generate_lots_for_subcontract() + if lots: + generated_count += len(lots) + generated_lots.extend([lot.name for lot in lots]) + except Exception as e: + _logger.error(f"Error generating lots for move {move.id}: {e}") + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': 'Generation Error', + 'message': f'Error generating lots: {str(e)}', + 'type': 'danger', + 'sticky': True, + } + } + + if generated_count > 0: + message = f'Generated {generated_count} lots: {", ".join(generated_lots[:5])}' + if len(generated_lots) > 5: + message += f' and {len(generated_lots) - 5} more...' + + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': 'Success', + 'message': message, + 'type': 'success', + 'sticky': False, + } + } + else: + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': 'No Lots Generated', + 'message': 'No lots were generated. Check that products have lot/serial tracking and custom sequences configured.', + 'type': 'info', + 'sticky': False, + } + } + + def _auto_assign_lots_on_subcontract_receipt(self): + """ + Automatically assign lot numbers when validating subcontract receipts. + This is called during the validation process. + """ + for picking in self: + if picking.picking_type_code == 'incoming': + subcontract_moves = picking.move_ids.filtered('is_subcontract') + for move in subcontract_moves: + if move.product_id.tracking in ['lot', 'serial'] and move.state not in ['done', 'cancel']: + move._auto_generate_lots_for_subcontract() + + def action_open_subcontract_lot_wizard(self): + """Open the subcontract lot generator wizard.""" + self.ensure_one() + + # Find the first subcontract move for default values + subcontract_move = self.move_ids.filtered('is_subcontract')[:1] + + if not subcontract_move: + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': 'No Subcontract Moves', + 'message': 'This picking does not contain any subcontract moves.', + 'type': 'warning', + 'sticky': False, + } + } + + context = { + 'default_picking_id': self.id, + } + + if subcontract_move: + context.update({ + 'default_move_id': subcontract_move.id, + 'default_product_id': subcontract_move.product_id.id, + 'default_quantity': subcontract_move.product_uom_qty, + }) + + return { + 'name': 'Generate Subcontract Lots', + 'type': 'ir.actions.act_window', + 'res_model': 'subcontract.lot.generator', + 'view_mode': 'form', + 'target': 'new', + 'context': context, + } + + def button_validate(self): + """Override to auto-generate lots for subcontract moves before validation.""" + # Auto-generate lots for subcontract moves if needed + self._auto_assign_lots_on_subcontract_receipt() + return super().button_validate() \ No newline at end of file diff --git a/security/ir.model.access.csv b/security/ir.model.access.csv new file mode 100644 index 0000000..5f77332 --- /dev/null +++ b/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_subcontract_lot_generator,subcontract.lot.generator,model_subcontract_lot_generator,stock.group_stock_user,1,1,1,1 \ No newline at end of file diff --git a/tests/test_subcontract_lots.py b/tests/test_subcontract_lots.py new file mode 100644 index 0000000..4509e19 --- /dev/null +++ b/tests/test_subcontract_lots.py @@ -0,0 +1,189 @@ +from odoo.tests.common import TransactionCase +from odoo.exceptions import UserError + + +class TestSubcontractLots(TransactionCase): + + def setUp(self): + super().setUp() + + # Create a product with lot tracking + self.product = self.env['product.product'].create({ + 'name': 'Subcontract Test Product', + 'type': 'product', + 'tracking': 'lot', + }) + + # Create a custom sequence for the product + self.sequence = self.env['ir.sequence'].create({ + 'name': 'Test Subcontract Sequence', + 'code': 'stock.lot.serial', + 'prefix': 'SUB-%(y)s-', + 'padding': 4, + }) + + self.product.product_tmpl_id.lot_sequence_id = self.sequence + + # Create a subcontractor + self.subcontractor = self.env['res.partner'].create({ + 'name': 'Test Subcontractor', + 'is_company': True, + }) + + # Create locations + self.location_stock = self.env.ref('stock.stock_location_stock') + self.location_subcontractor = self.env['stock.location'].create({ + 'name': 'Subcontractor Location', + 'usage': 'supplier', + 'partner_id': self.subcontractor.id, + }) + + def test_auto_generate_lots_subcontract(self): + """Test automatic lot generation for subcontract moves.""" + + # Create a subcontract receipt + picking = self.env['stock.picking'].create({ + 'picking_type_id': self.env.ref('stock.picking_type_in').id, + 'location_id': self.location_subcontractor.id, + 'location_dest_id': self.location_stock.id, + 'partner_id': self.subcontractor.id, + }) + + # Create a subcontract move + move = self.env['stock.move'].create({ + 'name': 'Test Subcontract Move', + 'product_id': self.product.id, + 'product_uom_qty': 5.0, + 'product_uom': self.product.uom_id.id, + 'picking_id': picking.id, + 'location_id': self.location_subcontractor.id, + 'location_dest_id': self.location_stock.id, + 'is_subcontract': True, + }) + + # Test auto-generation + move._auto_generate_lots_for_subcontract() + + # Check that a lot was created + self.assertEqual(len(move.move_line_ids), 1) + self.assertTrue(move.move_line_ids[0].lot_id) + self.assertTrue(move.move_line_ids[0].lot_id.name.startswith('SUB-')) + + def test_subcontract_lot_generator_wizard(self): + """Test the subcontract lot generator wizard.""" + + # Create a picking and move + picking = self.env['stock.picking'].create({ + 'picking_type_id': self.env.ref('stock.picking_type_in').id, + 'location_id': self.location_subcontractor.id, + 'location_dest_id': self.location_stock.id, + 'partner_id': self.subcontractor.id, + }) + + move = self.env['stock.move'].create({ + 'name': 'Test Move', + 'product_id': self.product.id, + 'product_uom_qty': 10.0, + 'product_uom': self.product.uom_id.id, + 'picking_id': picking.id, + 'location_id': self.location_subcontractor.id, + 'location_dest_id': self.location_stock.id, + 'is_subcontract': True, + }) + + # Create wizard + wizard = self.env['subcontract.lot.generator'].create({ + 'picking_id': picking.id, + 'move_id': move.id, + 'product_id': self.product.id, + 'quantity': 10.0, + 'lot_count': 2, + 'use_sequence': True, + }) + + # Generate lots + wizard.action_generate_lots() + + # Check results + self.assertEqual(len(move.move_line_ids), 2) + for line in move.move_line_ids: + self.assertTrue(line.lot_id) + self.assertTrue(line.lot_id.name.startswith('SUB-')) + + def test_serial_tracking_subcontract(self): + """Test serial tracking for subcontract products.""" + + # Change product to serial tracking + self.product.tracking = 'serial' + + # Create picking and move + picking = self.env['stock.picking'].create({ + 'picking_type_id': self.env.ref('stock.picking_type_in').id, + 'location_id': self.location_subcontractor.id, + 'location_dest_id': self.location_stock.id, + 'partner_id': self.subcontractor.id, + }) + + move = self.env['stock.move'].create({ + 'name': 'Test Serial Move', + 'product_id': self.product.id, + 'product_uom_qty': 3.0, + 'product_uom': self.product.uom_id.id, + 'picking_id': picking.id, + 'location_id': self.location_subcontractor.id, + 'location_dest_id': self.location_stock.id, + 'is_subcontract': True, + }) + + # Test auto-generation for serial tracking + move._auto_generate_lots_for_subcontract() + + # Should create 3 move lines (one per serial) + self.assertEqual(len(move.move_line_ids), 3) + for line in move.move_line_ids: + self.assertEqual(line.product_uom_qty, 1.0) + self.assertTrue(line.lot_id) + self.assertTrue(line.lot_id.name.startswith('SUB-')) + + def test_picking_auto_generate_action(self): + """Test the picking-level auto-generate action.""" + + # Create picking with multiple subcontract moves + picking = self.env['stock.picking'].create({ + 'picking_type_id': self.env.ref('stock.picking_type_in').id, + 'location_id': self.location_subcontractor.id, + 'location_dest_id': self.location_stock.id, + 'partner_id': self.subcontractor.id, + }) + + # Create two moves + move1 = self.env['stock.move'].create({ + 'name': 'Test Move 1', + 'product_id': self.product.id, + 'product_uom_qty': 2.0, + 'product_uom': self.product.uom_id.id, + 'picking_id': picking.id, + 'location_id': self.location_subcontractor.id, + 'location_dest_id': self.location_stock.id, + 'is_subcontract': True, + }) + + move2 = self.env['stock.move'].create({ + 'name': 'Test Move 2', + 'product_id': self.product.id, + 'product_uom_qty': 3.0, + 'product_uom': self.product.uom_id.id, + 'picking_id': picking.id, + 'location_id': self.location_subcontractor.id, + 'location_dest_id': self.location_stock.id, + 'is_subcontract': True, + }) + + # Test picking-level auto-generation + picking.action_auto_generate_lots_subcontract() + + # Check that both moves have lots generated + self.assertTrue(move1.move_line_ids) + self.assertTrue(move2.move_line_ids) + self.assertTrue(all(line.lot_id for line in move1.move_line_ids)) + self.assertTrue(all(line.lot_id for line in move2.move_line_ids)) \ No newline at end of file diff --git a/views/stock_move_views.xml b/views/stock_move_views.xml new file mode 100644 index 0000000..928cc9b --- /dev/null +++ b/views/stock_move_views.xml @@ -0,0 +1,20 @@ + + + + + stock.move.form.inherit.lot.generation + stock.move + + + + + + + + \ No newline at end of file diff --git a/wizard/__init__.py b/wizard/__init__.py new file mode 100644 index 0000000..65670cc --- /dev/null +++ b/wizard/__init__.py @@ -0,0 +1 @@ +from . import subcontract_lot_generator \ No newline at end of file diff --git a/wizard/subcontract_lot_generator.py b/wizard/subcontract_lot_generator.py new file mode 100644 index 0000000..4b4daff --- /dev/null +++ b/wizard/subcontract_lot_generator.py @@ -0,0 +1,111 @@ +from odoo import api, fields, models, _ +from odoo.exceptions import UserError + + +class SubcontractLotGenerator(models.TransientModel): + _name = 'subcontract.lot.generator' + _description = 'Subcontract Lot Generator Wizard' + + picking_id = fields.Many2one('stock.picking', string='Picking', required=True) + move_id = fields.Many2one('stock.move', string='Move', required=True) + product_id = fields.Many2one('product.product', string='Product', required=True) + quantity = fields.Float('Quantity', required=True, default=1.0) + lot_count = fields.Integer('Number of Lots', default=1, help='Number of lots to generate') + tracking = fields.Selection(related='product_id.tracking') + use_sequence = fields.Boolean('Use Product Sequence', default=True) + custom_prefix = fields.Char('Custom Prefix', help='Override the product sequence prefix') + + @api.onchange('product_id') + def _onchange_product_id(self): + if self.product_id: + if self.product_id.tracking == 'serial': + self.lot_count = int(self.quantity) + else: + self.lot_count = 1 + + @api.onchange('quantity', 'tracking') + def _onchange_quantity(self): + if self.tracking == 'serial': + self.lot_count = int(self.quantity) + + def action_generate_lots(self): + """Generate lots based on wizard configuration.""" + self.ensure_one() + + if not self.product_id.tracking in ['lot', 'serial']: + raise UserError(_('Product must have lot or serial tracking enabled.')) + + if self.lot_count <= 0: + raise UserError(_('Number of lots must be greater than 0.')) + + # Get the sequence + tmpl = self.product_id.product_tmpl_id + lot_sequence = getattr(tmpl, 'lot_sequence_id', False) + + if not lot_sequence and self.use_sequence: + raise UserError(_('No lot sequence configured for product %s') % self.product_id.display_name) + + # Generate lot names + if self.use_sequence and lot_sequence: + if self.custom_prefix: + # Temporarily override the sequence prefix + original_prefix = lot_sequence.prefix + lot_sequence.prefix = self.custom_prefix + + # Use the optimized batch generation + if self.lot_count > 10: + lot_names = self.move_id._allocate_sequence_batch(lot_sequence, self.lot_count) + else: + lot_names = [lot_sequence.next_by_id() for _ in range(self.lot_count)] + + if self.custom_prefix: + # Restore original prefix + lot_sequence.prefix = original_prefix + else: + # Generate simple sequential names + lot_names = [f"LOT-{i+1:04d}" for i in range(self.lot_count)] + + # Create the lots + Lot = self.env['stock.lot'] + lot_vals_list = [] + for lot_name in lot_names: + lot_vals = { + 'name': lot_name, + 'product_id': self.product_id.id, + 'company_id': self.picking_id.company_id.id, + } + lot_vals_list.append(lot_vals) + + lots = Lot.create(lot_vals_list) + + # Create move lines + if self.tracking == 'serial': + # One move line per lot for serial tracking + qty_per_lot = 1.0 + else: + # Distribute quantity across lots for lot tracking + qty_per_lot = self.quantity / self.lot_count + + for lot in lots: + self.env['stock.move.line'].create({ + 'move_id': self.move_id.id, + 'product_id': self.product_id.id, + 'lot_id': lot.id, + 'product_uom_qty': qty_per_lot, + 'qty_done': 0.0, + 'product_uom_id': self.move_id.product_uom.id, + 'location_id': self.move_id.location_id.id, + 'location_dest_id': self.move_id.location_dest_id.id, + 'picking_id': self.picking_id.id, + }) + + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Success'), + 'message': _('Generated %d lots for %s') % (len(lots), self.product_id.display_name), + 'type': 'success', + 'sticky': False, + } + } \ No newline at end of file diff --git a/wizard/subcontract_lot_generator_views.xml b/wizard/subcontract_lot_generator_views.xml new file mode 100644 index 0000000..c5dcd78 --- /dev/null +++ b/wizard/subcontract_lot_generator_views.xml @@ -0,0 +1,41 @@ + + + + + subcontract.lot.generator.form + subcontract.lot.generator + +
+ + + + + + + + + + + + + + +
+
+
+
+
+ + + + Generate Subcontract Lots + subcontract.lot.generator + form + new + { + 'default_picking_id': active_id, + } + +
\ No newline at end of file