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/views/stock_picking_views.xml b/views/stock_picking_views.xml
new file mode 100644
index 0000000..b00699b
--- /dev/null
+++ b/views/stock_picking_views.xml
@@ -0,0 +1,69 @@
+
+
+
+
+ stock.picking.form.inherit.subcontract.lots
+ stock.picking
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ stock.move.line.detailed.operation.inherit.subcontract
+ stock.move.line
+
+
+
+
+
+
+
+
+
+
+
+ stock.picking.form.subcontract.lots.smart.button
+ stock.picking
+
+
+
+
+
+
+
+
\ 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