first commit
This commit is contained in:
commit
56cd97ef27
64
.gitignore
vendored
Normal file
64
.gitignore
vendored
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
.hypothesis/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# celery beat schedule file
|
||||||
|
celerybeat-schedule
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Odoo
|
||||||
|
*.log
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.DS_Store
|
||||||
33
README.md
Normal file
33
README.md
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# Subcontract Generate Lot
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This Odoo 19 module provides automated lot/serial number generation specifically for subcontracting receipts. It isolates the subcontracting automation feature from legacy modules, ensuring compatibility with Odoo 19's native "Lot Sequence per Product" functionality.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- **Auto-Generate Lots**: Automatically generates lot/serial numbers for subcontract receipt moves based on the product's configured sequence.
|
||||||
|
- **Wizard Support**: Includes a "Generate Subcontract Lots" wizard for manual control and batch generation.
|
||||||
|
- **Validation Hook**: Automatically triggers lot generation when validating a subcontracting receipt (Stock Picking).
|
||||||
|
- **Native Compatibility**: Uses Odoo 19's standard `lot_sequence_id` on the product template, ensuring no conflict with standard features.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
- **Odoo Version**: 19.0
|
||||||
|
- **Dependencies**:
|
||||||
|
- `stock`
|
||||||
|
- `mrp`
|
||||||
|
- `mrp_subcontracting`
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
1. Place the module in your custom addons path.
|
||||||
|
2. Update the **App List** in Odoo.
|
||||||
|
3. Install **Subcontract Generate Lot**.
|
||||||
|
4. **Important**: If you are migrating from `product_lot_sequence_per_product`, you MUST uninstall that module after installing this one to prevent conflicts.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
1. **Configure Product**:
|
||||||
|
- Go to a Product form -> **Inventory** tab.
|
||||||
|
- Set **Tracking** to 'By Lots' or 'By Unique Serial Number'.
|
||||||
|
- Configure the **Lot/Serial Numbers Sequence** (Standard Odoo field).
|
||||||
|
2. **Subcontract Receipt**:
|
||||||
|
- Create a Purchase Order for a subcontracted product.
|
||||||
|
- Confirm the PO to create a Receipt (Stock Picking).
|
||||||
|
- On the Receipt, use the **Auto Generate Lots** button or validate the transfer to trigger generation.
|
||||||
2
__init__.py
Normal file
2
__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
from . import models
|
||||||
|
from . import wizard
|
||||||
21
__manifest__.py
Normal file
21
__manifest__.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
'name': 'Subcontract Generate Lot',
|
||||||
|
'version': '1.0',
|
||||||
|
'category': 'Inventory/Manufacturing',
|
||||||
|
'summary': 'Auto-generate lot numbers for subcontract moves',
|
||||||
|
'description': """
|
||||||
|
Auto-generates lot numbers for subcontract moves using the standard product lot sequence.
|
||||||
|
This functionality was extracted from the legacy 'product_lot_sequence_per_product' module
|
||||||
|
to coexist with Odoo 19's native lot per product feature.
|
||||||
|
""",
|
||||||
|
'depends': ['base', 'stock', 'mrp', 'mrp_subcontracting'],
|
||||||
|
'data': [
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
'views/stock_picking_views.xml',
|
||||||
|
'views/stock_move_views.xml',
|
||||||
|
'wizard/subcontract_lot_generator_views.xml',
|
||||||
|
],
|
||||||
|
'installable': True,
|
||||||
|
'application': False,
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
}
|
||||||
3
models/__init__.py
Normal file
3
models/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from . import stock_move
|
||||||
|
from . import stock_picking
|
||||||
|
from . import stock_move_line
|
||||||
126
models/stock_move.py
Normal file
126
models/stock_move.py
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
from odoo import api, models, _
|
||||||
|
import logging
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class StockMove(models.Model):
|
||||||
|
_inherit = 'stock.move'
|
||||||
|
|
||||||
|
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
|
||||||
|
# Use standard Odoo 19 lot_sequence_id field
|
||||||
|
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 standard sequence
|
||||||
|
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
|
||||||
|
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 getattr(self, 'is_subcontract', False):
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
}
|
||||||
83
models/stock_move_line.py
Normal file
83
models/stock_move_line.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
from odoo import api, models
|
||||||
|
|
||||||
|
|
||||||
|
class StockMoveLine(models.Model):
|
||||||
|
_inherit = 'stock.move.line'
|
||||||
|
|
||||||
|
def _prepare_new_lot_vals(self):
|
||||||
|
"""
|
||||||
|
Ensure that bogus base like '0' or empty lot_name does not create a lot literally named '0'.
|
||||||
|
If lot_name is empty or equals '0', let standard Odoo logic handles it
|
||||||
|
(or if previously relied on custom overrides, we ensure we pass False to trigger sequence usage if available).
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
# Normalize lot_name
|
||||||
|
lot_name = (self.lot_name or '').strip()
|
||||||
|
normalized_name = lot_name if lot_name and lot_name != '0' else False
|
||||||
|
|
||||||
|
vals = {
|
||||||
|
'name': normalized_name,
|
||||||
|
'product_id': self.product_id.id,
|
||||||
|
}
|
||||||
|
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
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Odoo 19 standardizing on 'quantity' for stock.move.line
|
||||||
|
quantity_value = self.quantity or 0.0
|
||||||
|
src_uom = self.product_uom_id
|
||||||
|
|
||||||
|
if quantity_value <= 0:
|
||||||
|
# Fallback logic if line has no quantity
|
||||||
|
if self.move_id:
|
||||||
|
done = self.move_id.quantity
|
||||||
|
demand = self.move_id.product_uom_qty
|
||||||
|
remaining = max(demand - done, 0.0)
|
||||||
|
quantity_value = remaining if remaining > 0 else 1.0
|
||||||
|
src_uom = self.move_id.product_uom
|
||||||
|
else:
|
||||||
|
quantity_value = 1.0
|
||||||
|
src_uom = self.product_uom_id
|
||||||
|
|
||||||
|
# Return action to open wizard
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
}
|
||||||
185
models/stock_picking.py
Normal file
185
models/stock_picking.py
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
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')
|
||||||
|
def _compute_has_subcontract_moves(self):
|
||||||
|
"""Compute if this picking has any subcontract moves."""
|
||||||
|
for picking in self:
|
||||||
|
# Check for 'is_subcontract' field safely using getattr/mapped in case other modules affect it
|
||||||
|
subcontract_moves = picking.move_ids.filtered(lambda m: getattr(m, 'is_subcontract', False))
|
||||||
|
picking.has_subcontract_moves = bool(subcontract_moves)
|
||||||
|
|
||||||
|
@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(lambda m: getattr(m, 'is_subcontract', False))
|
||||||
|
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(lambda m: getattr(m, 'is_subcontract', False))
|
||||||
|
lot_ids = subcontract_moves.move_line_ids.mapped('lot_id').ids
|
||||||
|
|
||||||
|
if not lot_ids:
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.client',
|
||||||
|
'tag': 'display_notification',
|
||||||
|
'params': {
|
||||||
|
'title': 'No Lots Found',
|
||||||
|
'message': 'No lots have been generated for subcontract moves yet.',
|
||||||
|
'type': 'info',
|
||||||
|
'sticky': False,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'name': f'Generated Lots ({len(lot_ids)})',
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'res_model': 'stock.lot',
|
||||||
|
'view_mode': 'list,form',
|
||||||
|
'views': [(False, 'list'), (False, 'form')],
|
||||||
|
'domain': [('id', 'in', lot_ids)],
|
||||||
|
'context': {
|
||||||
|
'create': False,
|
||||||
|
'edit': False,
|
||||||
|
},
|
||||||
|
'target': 'current',
|
||||||
|
}
|
||||||
|
|
||||||
|
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(lambda m: getattr(m, 'is_subcontract', False))
|
||||||
|
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(lambda m: getattr(m, 'is_subcontract', False))
|
||||||
|
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(lambda m: getattr(m, 'is_subcontract', False))[: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()
|
||||||
1
wizard/__init__.py
Normal file
1
wizard/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from . import subcontract_lot_generator
|
||||||
109
wizard/subcontract_lot_generator.py
Normal file
109
wizard/subcontract_lot_generator.py
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
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)
|
||||||
|
|
||||||
|
@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:
|
||||||
|
# Use standard Odoo sequence generation
|
||||||
|
# Removed custom batch allocation and unsafe prefix overrides
|
||||||
|
lot_names = [lot_sequence.next_by_id() for _ in range(self.lot_count)]
|
||||||
|
else:
|
||||||
|
# Generate simple sequential names fallback
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Calculate quantity in Product's Base UOM
|
||||||
|
# self.quantity is in self.move_id.product_uom (or whatever was passed)
|
||||||
|
base_uom = self.product_id.uom_id
|
||||||
|
move_uom = self.move_id.product_uom
|
||||||
|
|
||||||
|
total_base_qty = self.quantity
|
||||||
|
if move_uom and base_uom and move_uom != base_uom:
|
||||||
|
total_base_qty = move_uom._compute_quantity(self.quantity, base_uom)
|
||||||
|
|
||||||
|
if self.tracking == 'serial':
|
||||||
|
# One move line per lot for serial tracking, usually 1 unit of base UOM per serial
|
||||||
|
qty_per_lot = 1.0
|
||||||
|
else:
|
||||||
|
# Distribute quantity across lots for lot tracking
|
||||||
|
qty_per_lot = total_base_qty / self.lot_count
|
||||||
|
|
||||||
|
for lot in lots:
|
||||||
|
vals = {
|
||||||
|
'move_id': self.move_id.id,
|
||||||
|
'product_id': self.product_id.id,
|
||||||
|
'lot_id': lot.id,
|
||||||
|
'quantity': qty_per_lot, # Odoo 19 uses 'quantity' instead of product_uom_qty/qty_done in stock.move.line for initial demand?
|
||||||
|
# Let's check standard stock.move.line fields. Usually it's quantity (done) or reserved_uom_qty.
|
||||||
|
# In Odoo 19 stock.move.line has 'quantity'.
|
||||||
|
'product_uom_id': base_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,
|
||||||
|
}
|
||||||
|
self.env['stock.move.line'].create(vals)
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user