Compare commits

...

1 Commits
main ... 19.0

Author SHA1 Message Date
38b3844e61 fix for odoo 19 2025-11-12 15:57:46 +07:00
17 changed files with 339 additions and 338 deletions

View File

@ -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

View File

@ -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,
}

Binary file not shown.

View File

@ -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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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,
}

View File

@ -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'

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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>