Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 38b3844e61 |
82
README.md
82
README.md
@ -1,42 +1,42 @@
|
||||
# Product Lot Sequence Per Product
|
||||
|
||||
This module extends Odoo's lot and serial number generation to support unique sequences per product, aligning Odoo 18 with the behavior introduced in Odoo 19.
|
||||
|
||||
## Features
|
||||
|
||||
- **Per-Product Sequence Configuration**: Define a unique sequence for lot/serial number generation for each product.
|
||||
- **Inventory Tab Integration**: Configure the custom sequence directly on the product form under the Inventory tab.
|
||||
- **Automatic Generation**: Lot/serial numbers generated during incoming receipts and manufacturing orders follow the product-specific sequence.
|
||||
- **Fallback Mechanism**: If no sequence is defined for a product, it falls back to the global lot/serial sequence.
|
||||
- **UI Enhancements**: Avoids generation of invalid "0" lot numbers in manual and wizard flows.
|
||||
|
||||
## Configuration
|
||||
|
||||
1. Navigate to **Inventory > Products**.
|
||||
2. Open or create a product.
|
||||
3. Go to the **Inventory** tab.
|
||||
4. Set the **Custom Lot/Serial** prefix to define a new sequence or select an existing sequence from the **Lot Sequence** field.
|
||||
5. The **Next Number** field displays the next lot/serial number that will be generated.
|
||||
|
||||
## Behavior
|
||||
|
||||
- **Incoming Shipments**: When receiving products, if a product has a custom sequence, the generated lot/serial numbers will follow this sequence.
|
||||
- **Manufacturing Orders**: When producing products, the finished lots/serials will be generated using the product's custom sequence.
|
||||
- **Manual Creation**: Creating lots/serials manually or via "Generate Serials/Lots" will respect the product's sequence if configured.
|
||||
|
||||
## Technical Details
|
||||
|
||||
- The module adds a `lot_sequence_id` field to `product.template` to link the sequence.
|
||||
- It overrides the `stock.lot` creation to use the product's sequence.
|
||||
- It extends `stock.move` and `stock.move.line` to handle UI inputs and normalize "0" or empty inputs.
|
||||
- It overrides `mrp.production._prepare_stock_lot_values` to ensure manufacturing flows use the product sequence.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `stock`
|
||||
- `mrp`
|
||||
|
||||
## Compatibility
|
||||
|
||||
- Odoo 18
|
||||
# Product Lot Sequence Per Product
|
||||
|
||||
This module extends Odoo's lot and serial number generation to support unique sequences per product, aligning Odoo 18 with the behavior introduced in Odoo 19.
|
||||
|
||||
## Features
|
||||
|
||||
- **Per-Product Sequence Configuration**: Define a unique sequence for lot/serial number generation for each product.
|
||||
- **Inventory Tab Integration**: Configure the custom sequence directly on the product form under the Inventory tab.
|
||||
- **Automatic Generation**: Lot/serial numbers generated during incoming receipts and manufacturing orders follow the product-specific sequence.
|
||||
- **Fallback Mechanism**: If no sequence is defined for a product, it falls back to the global lot/serial sequence.
|
||||
- **UI Enhancements**: Avoids generation of invalid "0" lot numbers in manual and wizard flows.
|
||||
|
||||
## Configuration
|
||||
|
||||
1. Navigate to **Inventory > Products**.
|
||||
2. Open or create a product.
|
||||
3. Go to the **Inventory** tab.
|
||||
4. Set the **Custom Lot/Serial** prefix to define a new sequence or select an existing sequence from the **Lot Sequence** field.
|
||||
5. The **Next Number** field displays the next lot/serial number that will be generated.
|
||||
|
||||
## Behavior
|
||||
|
||||
- **Incoming Shipments**: When receiving products, if a product has a custom sequence, the generated lot/serial numbers will follow this sequence.
|
||||
- **Manufacturing Orders**: When producing products, the finished lots/serials will be generated using the product's custom sequence.
|
||||
- **Manual Creation**: Creating lots/serials manually or via "Generate Serials/Lots" will respect the product's sequence if configured.
|
||||
|
||||
## Technical Details
|
||||
|
||||
- The module adds a `lot_sequence_id` field to `product.template` to link the sequence.
|
||||
- It overrides the `stock.lot` creation to use the product's sequence.
|
||||
- It extends `stock.move` and `stock.move.line` to handle UI inputs and normalize "0" or empty inputs.
|
||||
- It overrides `mrp.production._prepare_stock_lot_values` to ensure manufacturing flows use the product sequence.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `stock`
|
||||
- `mrp`
|
||||
|
||||
## Compatibility
|
||||
|
||||
- Odoo 18
|
||||
- Requires `mrp` module for manufacturing order support
|
||||
@ -1,14 +1,14 @@
|
||||
{
|
||||
'name': 'Product Lot Sequence Per Product',
|
||||
'version': '1.0',
|
||||
'depends': [
|
||||
'stock',
|
||||
'mrp',
|
||||
],
|
||||
'data': [
|
||||
'views/product_views.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
'application': False,
|
||||
{
|
||||
'name': 'Product Lot Sequence Per Product',
|
||||
'version': '1.0',
|
||||
'depends': [
|
||||
'stock',
|
||||
'mrp',
|
||||
],
|
||||
'data': [
|
||||
'views/product_views.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
'application': False,
|
||||
}
|
||||
BIN
__pycache__/__init__.cpython-310.pyc
Normal file
BIN
__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
@ -1,5 +1,5 @@
|
||||
from . import product_template
|
||||
from . import stock_move
|
||||
from . import stock_lot
|
||||
from . import stock_move_line
|
||||
from . import product_template
|
||||
from . import stock_move
|
||||
from . import stock_lot
|
||||
from . import stock_move_line
|
||||
from . import mrp_production
|
||||
BIN
models/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
models/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
models/__pycache__/mrp_production.cpython-310.pyc
Normal file
BIN
models/__pycache__/mrp_production.cpython-310.pyc
Normal file
Binary file not shown.
BIN
models/__pycache__/product_template.cpython-310.pyc
Normal file
BIN
models/__pycache__/product_template.cpython-310.pyc
Normal file
Binary file not shown.
BIN
models/__pycache__/stock_lot.cpython-310.pyc
Normal file
BIN
models/__pycache__/stock_lot.cpython-310.pyc
Normal file
Binary file not shown.
BIN
models/__pycache__/stock_move.cpython-310.pyc
Normal file
BIN
models/__pycache__/stock_move.cpython-310.pyc
Normal file
Binary file not shown.
BIN
models/__pycache__/stock_move_line.cpython-310.pyc
Normal file
BIN
models/__pycache__/stock_move_line.cpython-310.pyc
Normal file
Binary file not shown.
@ -1,49 +1,49 @@
|
||||
from odoo import models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class MrpProduction(models.Model):
|
||||
_inherit = 'mrp.production'
|
||||
|
||||
def _prepare_stock_lot_values(self):
|
||||
self.ensure_one()
|
||||
name = False
|
||||
product = self.product_id
|
||||
tmpl = product.product_tmpl_id
|
||||
seq = getattr(tmpl, 'lot_sequence_id', False)
|
||||
if seq:
|
||||
tried = set()
|
||||
# Try generating a unique candidate from the per-product sequence
|
||||
for _i in range(10):
|
||||
candidate = seq.next_by_id()
|
||||
if not candidate:
|
||||
break
|
||||
if candidate in tried:
|
||||
continue
|
||||
tried.add(candidate)
|
||||
exist_lot = self.env['stock.lot'].search([
|
||||
('product_id', '=', product.id),
|
||||
'|', ('company_id', '=', False), ('company_id', '=', self.company_id.id),
|
||||
('name', '=', candidate),
|
||||
], limit=1)
|
||||
if not exist_lot:
|
||||
name = candidate
|
||||
break
|
||||
|
||||
# Fallback to default behavior if no per-product candidate was found
|
||||
if not name:
|
||||
name = self.env['ir.sequence'].next_by_code('stock.lot.serial')
|
||||
exist_lot = (not name) or self.env['stock.lot'].search([
|
||||
('product_id', '=', product.id),
|
||||
'|', ('company_id', '=', False), ('company_id', '=', self.company_id.id),
|
||||
('name', '=', name),
|
||||
], limit=1)
|
||||
if exist_lot:
|
||||
name = self.env['stock.lot']._get_next_serial(self.company_id, product)
|
||||
if not name:
|
||||
raise UserError(_("Please set the first Serial Number or a default sequence"))
|
||||
|
||||
return {
|
||||
'product_id': product.id,
|
||||
'name': name,
|
||||
from odoo import models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class MrpProduction(models.Model):
|
||||
_inherit = 'mrp.production'
|
||||
|
||||
def _prepare_stock_lot_values(self):
|
||||
self.ensure_one()
|
||||
name = False
|
||||
product = self.product_id
|
||||
tmpl = product.product_tmpl_id
|
||||
seq = getattr(tmpl, 'lot_sequence_id', False)
|
||||
if seq:
|
||||
tried = set()
|
||||
# Try generating a unique candidate from the per-product sequence
|
||||
for _i in range(10):
|
||||
candidate = seq.next_by_id()
|
||||
if not candidate:
|
||||
break
|
||||
if candidate in tried:
|
||||
continue
|
||||
tried.add(candidate)
|
||||
exist_lot = self.env['stock.lot'].search([
|
||||
('product_id', '=', product.id),
|
||||
'|', ('company_id', '=', False), ('company_id', '=', self.company_id.id),
|
||||
('name', '=', candidate),
|
||||
], limit=1)
|
||||
if not exist_lot:
|
||||
name = candidate
|
||||
break
|
||||
|
||||
# Fallback to default behavior if no per-product candidate was found
|
||||
if not name:
|
||||
name = self.env['ir.sequence'].next_by_code('stock.lot.serial')
|
||||
exist_lot = (not name) or self.env['stock.lot'].search([
|
||||
('product_id', '=', product.id),
|
||||
'|', ('company_id', '=', False), ('company_id', '=', self.company_id.id),
|
||||
('name', '=', name),
|
||||
], limit=1)
|
||||
if exist_lot:
|
||||
name = self.env['stock.lot']._get_next_serial(self.company_id, product)
|
||||
if not name:
|
||||
raise UserError(_("Please set the first Serial Number or a default sequence"))
|
||||
|
||||
return {
|
||||
'product_id': product.id,
|
||||
'name': name,
|
||||
}
|
||||
@ -1,63 +1,63 @@
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class ProductTemplate(models.Model):
|
||||
_inherit = 'product.template'
|
||||
|
||||
lot_sequence_id = fields.Many2one(
|
||||
'ir.sequence',
|
||||
string='Serial/Lot Numbers Sequence',
|
||||
domain=[('code', '=', 'stock.lot.serial')],
|
||||
help='Technical Field: The Ir.Sequence record that is used to generate serial/lot numbers for this product'
|
||||
)
|
||||
serial_prefix_format = fields.Char(
|
||||
'Custom Lot/Serial',
|
||||
compute='_compute_serial_prefix_format',
|
||||
inverse='_inverse_serial_prefix_format',
|
||||
help='Set a prefix to generate serial/lot numbers automatically when receiving or producing this product. '
|
||||
'Use % codes like %(y)s for year, %(month)s for month, etc.'
|
||||
)
|
||||
next_serial = fields.Char(
|
||||
'Next Number',
|
||||
compute='_compute_next_serial',
|
||||
help='The next serial/lot number to be generated for this product'
|
||||
)
|
||||
|
||||
@api.depends('lot_sequence_id', 'lot_sequence_id.prefix')
|
||||
def _compute_serial_prefix_format(self):
|
||||
for template in self:
|
||||
template.serial_prefix_format = template.lot_sequence_id.prefix or ""
|
||||
|
||||
def _inverse_serial_prefix_format(self):
|
||||
valid_sequences = self.env['ir.sequence'].search([('prefix', 'in', self.mapped('serial_prefix_format'))])
|
||||
sequences_by_prefix = {seq.prefix: seq for seq in valid_sequences}
|
||||
for template in self:
|
||||
if template.serial_prefix_format:
|
||||
if template.serial_prefix_format in sequences_by_prefix:
|
||||
template.lot_sequence_id = sequences_by_prefix[template.serial_prefix_format]
|
||||
else:
|
||||
# Create a new sequence with the given prefix
|
||||
new_sequence = self.env['ir.sequence'].create({
|
||||
'name': f'{template.name} Serial Sequence',
|
||||
'code': 'stock.lot.serial',
|
||||
'prefix': template.serial_prefix_format,
|
||||
'padding': 7,
|
||||
'company_id': False, # Global sequence to avoid cross-company conflicts
|
||||
})
|
||||
template.lot_sequence_id = new_sequence
|
||||
sequences_by_prefix[template.serial_prefix_format] = new_sequence
|
||||
else:
|
||||
# Reset to default if no prefix
|
||||
template.lot_sequence_id = self.env.ref('stock.sequence_production_lots', raise_if_not_found=False)
|
||||
|
||||
@api.depends('serial_prefix_format', 'lot_sequence_id')
|
||||
def _compute_next_serial(self):
|
||||
for template in self:
|
||||
if template.lot_sequence_id:
|
||||
template.next_serial = '{:0{}d}{}'.format(
|
||||
template.lot_sequence_id.number_next_actual,
|
||||
template.lot_sequence_id.padding,
|
||||
template.lot_sequence_id.suffix or ""
|
||||
)
|
||||
else:
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class ProductTemplate(models.Model):
|
||||
_inherit = 'product.template'
|
||||
|
||||
lot_sequence_id = fields.Many2one(
|
||||
'ir.sequence',
|
||||
string='Serial/Lot Numbers Sequence',
|
||||
domain=[('code', '=', 'stock.lot.serial')],
|
||||
help='Technical Field: The Ir.Sequence record that is used to generate serial/lot numbers for this product'
|
||||
)
|
||||
serial_prefix_format = fields.Char(
|
||||
'Custom Lot/Serial',
|
||||
compute='_compute_serial_prefix_format',
|
||||
inverse='_inverse_serial_prefix_format',
|
||||
help='Set a prefix to generate serial/lot numbers automatically when receiving or producing this product. '
|
||||
'Use % codes like %(y)s for year, %(month)s for month, etc.'
|
||||
)
|
||||
next_serial = fields.Char(
|
||||
'Next Number',
|
||||
compute='_compute_next_serial',
|
||||
help='The next serial/lot number to be generated for this product'
|
||||
)
|
||||
|
||||
@api.depends('lot_sequence_id', 'lot_sequence_id.prefix')
|
||||
def _compute_serial_prefix_format(self):
|
||||
for template in self:
|
||||
template.serial_prefix_format = template.lot_sequence_id.prefix or ""
|
||||
|
||||
def _inverse_serial_prefix_format(self):
|
||||
valid_sequences = self.env['ir.sequence'].search([('prefix', 'in', self.mapped('serial_prefix_format'))])
|
||||
sequences_by_prefix = {seq.prefix: seq for seq in valid_sequences}
|
||||
for template in self:
|
||||
if template.serial_prefix_format:
|
||||
if template.serial_prefix_format in sequences_by_prefix:
|
||||
template.lot_sequence_id = sequences_by_prefix[template.serial_prefix_format]
|
||||
else:
|
||||
# Create a new sequence with the given prefix
|
||||
new_sequence = self.env['ir.sequence'].create({
|
||||
'name': f'{template.name} Serial Sequence',
|
||||
'code': 'stock.lot.serial',
|
||||
'prefix': template.serial_prefix_format,
|
||||
'padding': 7,
|
||||
'company_id': False, # Global sequence to avoid cross-company conflicts
|
||||
})
|
||||
template.lot_sequence_id = new_sequence
|
||||
sequences_by_prefix[template.serial_prefix_format] = new_sequence
|
||||
else:
|
||||
# Reset to default if no prefix
|
||||
template.lot_sequence_id = self.env.ref('stock.sequence_production_lots', raise_if_not_found=False)
|
||||
|
||||
@api.depends('serial_prefix_format', 'lot_sequence_id')
|
||||
def _compute_next_serial(self):
|
||||
for template in self:
|
||||
if template.lot_sequence_id:
|
||||
template.next_serial = '{:0{}d}{}'.format(
|
||||
template.lot_sequence_id.number_next_actual,
|
||||
template.lot_sequence_id.padding,
|
||||
template.lot_sequence_id.suffix or ""
|
||||
)
|
||||
else:
|
||||
template.next_serial = '00001'
|
||||
@ -1,19 +1,19 @@
|
||||
from odoo import api, models
|
||||
|
||||
|
||||
class StockLot(models.Model):
|
||||
_inherit = 'stock.lot'
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
# For each lot being created without a name, assign the next serial from the product's template sequence
|
||||
for vals in vals_list:
|
||||
if not vals.get('name') and vals.get('product_id'):
|
||||
product = self.env['product.product'].browse(vals['product_id'])
|
||||
seq = getattr(product.product_tmpl_id, 'lot_sequence_id', False)
|
||||
if seq:
|
||||
vals['name'] = seq.next_by_id()
|
||||
else:
|
||||
# Fallback to global sequence if no product sequence
|
||||
vals['name'] = self.env['ir.sequence'].next_by_code('stock.lot.serial')
|
||||
from odoo import api, models
|
||||
|
||||
|
||||
class StockLot(models.Model):
|
||||
_inherit = 'stock.lot'
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
# For each lot being created without a name, assign the next serial from the product's template sequence
|
||||
for vals in vals_list:
|
||||
if not vals.get('name') and vals.get('product_id'):
|
||||
product = self.env['product.product'].browse(vals['product_id'])
|
||||
seq = getattr(product.product_tmpl_id, 'lot_sequence_id', False)
|
||||
if seq:
|
||||
vals['name'] = seq.next_by_id()
|
||||
else:
|
||||
# Fallback to global sequence if no product sequence
|
||||
vals['name'] = self.env['ir.sequence'].next_by_code('stock.lot.serial')
|
||||
return super().create(vals_list)
|
||||
@ -1,71 +1,72 @@
|
||||
from odoo import api, models
|
||||
|
||||
|
||||
class StockMove(models.Model):
|
||||
_inherit = 'stock.move'
|
||||
|
||||
@api.onchange('product_id')
|
||||
def _onchange_product_id(self):
|
||||
"""Seed the next_serial field on stock.move when product changes, if product has a sequence."""
|
||||
res = super()._onchange_product_id()
|
||||
if self.product_id and getattr(self.product_id.product_tmpl_id, 'lot_sequence_id', False):
|
||||
self.next_serial = getattr(self.product_id.product_tmpl_id, 'next_serial', False)
|
||||
return res
|
||||
|
||||
def _create_lot_ids_from_move_line_vals(self, vals_list, product_id, company_id=False):
|
||||
"""
|
||||
Normalize incoming lot names during 'Generate Serials/Lots' or 'Import Serials/Lots'.
|
||||
- If user leaves '0' or empty as lot name, create lots without a name to let stock.lot.create()
|
||||
generate names from the product's per-product sequence (handled by our stock.lot override).
|
||||
- Otherwise, fallback to the standard behavior for explicit names.
|
||||
"""
|
||||
Lot = self.env['stock.lot']
|
||||
|
||||
# First handle entries that should be auto-generated (empty or '0')
|
||||
remaining_vals = []
|
||||
for vals in vals_list:
|
||||
lot_name = (vals.get('lot_name') or '').strip()
|
||||
if not lot_name or lot_name == '0':
|
||||
lot_vals = {
|
||||
'product_id': product_id,
|
||||
}
|
||||
if company_id:
|
||||
lot_vals['company_id'] = company_id
|
||||
# omit 'name' to trigger sequence in stock.lot.create() override
|
||||
lot = Lot.create([lot_vals])[0]
|
||||
vals['lot_id'] = lot.id
|
||||
vals['lot_name'] = False
|
||||
else:
|
||||
remaining_vals.append(vals)
|
||||
|
||||
# Delegate remaining with explicit names to the standard implementation
|
||||
if remaining_vals:
|
||||
return super()._create_lot_ids_from_move_line_vals(remaining_vals, product_id, company_id)
|
||||
return None
|
||||
|
||||
@api.model
|
||||
def action_generate_lot_line_vals(self, context, mode, first_lot, count, lot_text):
|
||||
"""
|
||||
If the 'Generate Serials/Lots' action is invoked with an empty or '0' base,
|
||||
generate names using the per-product sequence instead of stock.lot.generate_lot_names('0', n),
|
||||
which would yield 0,1,2...
|
||||
"""
|
||||
if mode == 'generate':
|
||||
product_id = context.get('default_product_id')
|
||||
if product_id:
|
||||
product = self.env['product.product'].browse(product_id)
|
||||
tmpl = product.product_tmpl_id
|
||||
if (not first_lot or first_lot == '0') and getattr(tmpl, 'lot_sequence_id', False):
|
||||
seq = tmpl.lot_sequence_id
|
||||
# Generate count names directly from the sequence
|
||||
generated_names = [seq.next_by_id() for _ in range(count or 0)]
|
||||
# Reuse parent implementation for the rest of the processing (locations, uom, etc.)
|
||||
# by passing a non-zero base and then overriding the names in the returned list.
|
||||
fake_first = 'SEQDUMMY-1'
|
||||
vals_list = super().action_generate_lot_line_vals(context, mode, fake_first, count, lot_text)
|
||||
# Overwrite the lot_name with sequence-based names; keep all other computed values (uom, putaway).
|
||||
for vals, name in zip(vals_list, generated_names):
|
||||
vals['lot_name'] = name
|
||||
return vals_list
|
||||
# Fallback to standard behavior
|
||||
from odoo import api, models
|
||||
|
||||
|
||||
class StockMove(models.Model):
|
||||
_inherit = 'stock.move'
|
||||
|
||||
@api.onchange('product_id')
|
||||
def _onchange_product_id(self):
|
||||
"""Seed the next_serial field on stock.move when product changes, if product has a sequence."""
|
||||
# Set next_serial based on product's lot sequence if available
|
||||
if self.product_id and getattr(self.product_id.product_tmpl_id, 'lot_sequence_id', False):
|
||||
self.next_serial = getattr(self.product_id.product_tmpl_id, 'next_serial', False)
|
||||
else:
|
||||
self.next_serial = False
|
||||
|
||||
def _create_lot_ids_from_move_line_vals(self, vals_list, product_id, company_id=False):
|
||||
"""
|
||||
Normalize incoming lot names during 'Generate Serials/Lots' or 'Import Serials/Lots'.
|
||||
- If user leaves '0' or empty as lot name, create lots without a name to let stock.lot.create()
|
||||
generate names from the product's per-product sequence (handled by our stock.lot override).
|
||||
- Otherwise, fallback to the standard behavior for explicit names.
|
||||
"""
|
||||
Lot = self.env['stock.lot']
|
||||
|
||||
# First handle entries that should be auto-generated (empty or '0')
|
||||
remaining_vals = []
|
||||
for vals in vals_list:
|
||||
lot_name = (vals.get('lot_name') or '').strip()
|
||||
if not lot_name or lot_name == '0':
|
||||
lot_vals = {
|
||||
'product_id': product_id,
|
||||
}
|
||||
if company_id:
|
||||
lot_vals['company_id'] = company_id
|
||||
# omit 'name' to trigger sequence in stock.lot.create() override
|
||||
lot = Lot.create([lot_vals])[0]
|
||||
vals['lot_id'] = lot.id
|
||||
vals['lot_name'] = False
|
||||
else:
|
||||
remaining_vals.append(vals)
|
||||
|
||||
# Delegate remaining with explicit names to the standard implementation
|
||||
if remaining_vals:
|
||||
return super()._create_lot_ids_from_move_line_vals(remaining_vals, product_id, company_id)
|
||||
return None
|
||||
|
||||
@api.model
|
||||
def action_generate_lot_line_vals(self, context, mode, first_lot, count, lot_text):
|
||||
"""
|
||||
If the 'Generate Serials/Lots' action is invoked with an empty or '0' base,
|
||||
generate names using the per-product sequence instead of stock.lot.generate_lot_names('0', n),
|
||||
which would yield 0,1,2...
|
||||
"""
|
||||
if mode == 'generate':
|
||||
product_id = context.get('default_product_id')
|
||||
if product_id:
|
||||
product = self.env['product.product'].browse(product_id)
|
||||
tmpl = product.product_tmpl_id
|
||||
if (not first_lot or first_lot == '0') and getattr(tmpl, 'lot_sequence_id', False):
|
||||
seq = tmpl.lot_sequence_id
|
||||
# Generate count names directly from the sequence
|
||||
generated_names = [seq.next_by_id() for _ in range(count or 0)]
|
||||
# Reuse parent implementation for the rest of the processing (locations, uom, etc.)
|
||||
# by passing a non-zero base and then overriding the names in the returned list.
|
||||
fake_first = 'SEQDUMMY-1'
|
||||
vals_list = super().action_generate_lot_line_vals(context, mode, fake_first, count, lot_text)
|
||||
# Overwrite the lot_name with sequence-based names; keep all other computed values (uom, putaway).
|
||||
for vals, name in zip(vals_list, generated_names):
|
||||
vals['lot_name'] = name
|
||||
return vals_list
|
||||
# Fallback to standard behavior
|
||||
return super().action_generate_lot_line_vals(context, mode, first_lot, count, lot_text)
|
||||
@ -1,24 +1,24 @@
|
||||
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 stock.lot.create() generate the name from
|
||||
the product's per-product sequence (handled by our stock.lot override).
|
||||
"""
|
||||
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, # False => triggers product sequence in stock.lot.create()
|
||||
'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
|
||||
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 stock.lot.create() generate the name from
|
||||
the product's per-product sequence (handled by our stock.lot override).
|
||||
"""
|
||||
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, # False => triggers product sequence in stock.lot.create()
|
||||
'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
|
||||
@ -1,39 +1,39 @@
|
||||
from odoo import api, models, fields, _
|
||||
from odoo.tools.float_utils import float_compare
|
||||
|
||||
|
||||
class StockQuant(models.Model):
|
||||
_inherit = 'stock.quant'
|
||||
|
||||
def _get_inventory_move_values(self, qty, location_id, location_dest_id, package_id=False, package_dest_id=False):
|
||||
"""Override to handle automatic lot generation for inventory adjustments."""
|
||||
# Check if we need to generate a lot for this inventory adjustment
|
||||
if (self.product_id.tracking in ['lot', 'serial'] and
|
||||
float_compare(qty, 0, precision_rounding=self.product_uom_id.rounding) > 0 and
|
||||
not self.lot_id and
|
||||
self.product_id.product_tmpl_id.lot_sequence_id):
|
||||
|
||||
# Generate lot number using the product's sequence
|
||||
lot_sequence = self.product_id.product_tmpl_id.lot_sequence_id
|
||||
lot_name = lot_sequence.next_by_id()
|
||||
|
||||
# Create the lot record
|
||||
lot = self.env['stock.lot'].create({
|
||||
'name': lot_name,
|
||||
'product_id': self.product_id.id,
|
||||
'company_id': self.company_id.id,
|
||||
})
|
||||
|
||||
# Update the quant with the new lot BEFORE creating the move
|
||||
self.lot_id = lot.id
|
||||
|
||||
# Call the original method to get the move values
|
||||
move_vals = super()._get_inventory_move_values(qty, location_id, location_dest_id, package_id, package_dest_id)
|
||||
|
||||
# Make sure the lot_id is properly set in the move line values
|
||||
if self.lot_id and 'move_line_ids' in move_vals:
|
||||
for line_command in move_vals['move_line_ids']:
|
||||
if line_command[0] in [0, 1] and line_command[2]: # create or update command
|
||||
line_command[2]['lot_id'] = self.lot_id.id
|
||||
|
||||
from odoo import api, models, fields, _
|
||||
from odoo.tools.float_utils import float_compare
|
||||
|
||||
|
||||
class StockQuant(models.Model):
|
||||
_inherit = 'stock.quant'
|
||||
|
||||
def _get_inventory_move_values(self, qty, location_id, location_dest_id, package_id=False, package_dest_id=False):
|
||||
"""Override to handle automatic lot generation for inventory adjustments."""
|
||||
# Check if we need to generate a lot for this inventory adjustment
|
||||
if (self.product_id.tracking in ['lot', 'serial'] and
|
||||
float_compare(qty, 0, precision_rounding=self.product_uom_id.rounding) > 0 and
|
||||
not self.lot_id and
|
||||
self.product_id.product_tmpl_id.lot_sequence_id):
|
||||
|
||||
# Generate lot number using the product's sequence
|
||||
lot_sequence = self.product_id.product_tmpl_id.lot_sequence_id
|
||||
lot_name = lot_sequence.next_by_id()
|
||||
|
||||
# Create the lot record
|
||||
lot = self.env['stock.lot'].create({
|
||||
'name': lot_name,
|
||||
'product_id': self.product_id.id,
|
||||
'company_id': self.company_id.id,
|
||||
})
|
||||
|
||||
# Update the quant with the new lot BEFORE creating the move
|
||||
self.lot_id = lot.id
|
||||
|
||||
# Call the original method to get the move values
|
||||
move_vals = super()._get_inventory_move_values(qty, location_id, location_dest_id, package_id, package_dest_id)
|
||||
|
||||
# Make sure the lot_id is properly set in the move line values
|
||||
if self.lot_id and 'move_line_ids' in move_vals:
|
||||
for line_command in move_vals['move_line_ids']:
|
||||
if line_command[0] in [0, 1] and line_command[2]: # create or update command
|
||||
line_command[2]['lot_id'] = self.lot_id.id
|
||||
|
||||
return move_vals
|
||||
@ -1,22 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Extend product template form view to add sequence fields on Inventory tab -->
|
||||
<record id="view_product_template_form_inherit_stock" model="ir.ui.view">
|
||||
<field name="name">product.template.form.inherit.stock.lot.sequence</field>
|
||||
<field name="model">product.template</field>
|
||||
<field name="inherit_id" ref="stock.view_template_property_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//page[@name='inventory']" position="inside">
|
||||
<group string="Traceability" name="traceability" groups="stock.group_production_lot" invisible="tracking == 'none'">
|
||||
<label for="serial_prefix_format" string="Custom Lot/Serial" invisible="tracking == 'none'"/>
|
||||
<div class="d-flex" invisible="tracking == 'none'">
|
||||
<field name="serial_prefix_format" style="max-width: 150px;"/>
|
||||
<field name="next_serial" style="max-width: 150px;"/>
|
||||
</div>
|
||||
<!-- Optionally show the technical sequence field -->
|
||||
<field name="lot_sequence_id" groups="base.group_no_one"/>
|
||||
</group>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Extend product template form view to add sequence fields on Inventory tab -->
|
||||
<record id="view_product_template_form_inherit_stock" model="ir.ui.view">
|
||||
<field name="name">product.template.form.inherit.stock.lot.sequence</field>
|
||||
<field name="model">product.template</field>
|
||||
<field name="inherit_id" ref="stock.view_template_property_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//page[@name='inventory']" position="inside">
|
||||
<group string="Traceability" name="traceability" groups="stock.group_production_lot" invisible="tracking == 'none'">
|
||||
<label for="serial_prefix_format" string="Custom Lot/Serial" invisible="tracking == 'none'"/>
|
||||
<div class="d-flex" invisible="tracking == 'none'">
|
||||
<field name="serial_prefix_format" style="max-width: 150px;"/>
|
||||
<field name="next_serial" style="max-width: 150px;"/>
|
||||
</div>
|
||||
<!-- Optionally show the technical sequence field -->
|
||||
<field name="lot_sequence_id" groups="base.group_no_one"/>
|
||||
</group>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Loading…
Reference in New Issue
Block a user