first commit
This commit is contained in:
commit
87b5351da2
1
__init__.py
Normal file
1
__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from . import models
|
||||
14
__manifest__.py
Normal file
14
__manifest__.py
Normal file
@ -0,0 +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,
|
||||
}
|
||||
BIN
__pycache__/__init__.cpython-312.pyc
Normal file
BIN
__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
5
models/__init__.py
Normal file
5
models/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
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-312.pyc
Normal file
BIN
models/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
models/__pycache__/mrp_production.cpython-312.pyc
Normal file
BIN
models/__pycache__/mrp_production.cpython-312.pyc
Normal file
Binary file not shown.
BIN
models/__pycache__/product_template.cpython-312.pyc
Normal file
BIN
models/__pycache__/product_template.cpython-312.pyc
Normal file
Binary file not shown.
BIN
models/__pycache__/stock_lot.cpython-312.pyc
Normal file
BIN
models/__pycache__/stock_lot.cpython-312.pyc
Normal file
Binary file not shown.
BIN
models/__pycache__/stock_move.cpython-312.pyc
Normal file
BIN
models/__pycache__/stock_move.cpython-312.pyc
Normal file
Binary file not shown.
BIN
models/__pycache__/stock_move_line.cpython-312.pyc
Normal file
BIN
models/__pycache__/stock_move_line.cpython-312.pyc
Normal file
Binary file not shown.
49
models/mrp_production.py
Normal file
49
models/mrp_production.py
Normal file
@ -0,0 +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,
|
||||
}
|
||||
63
models/product_template.py
Normal file
63
models/product_template.py
Normal file
@ -0,0 +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:
|
||||
template.next_serial = '00001'
|
||||
19
models/stock_lot.py
Normal file
19
models/stock_lot.py
Normal file
@ -0,0 +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')
|
||||
return super().create(vals_list)
|
||||
71
models/stock_move.py
Normal file
71
models/stock_move.py
Normal file
@ -0,0 +1,71 @@
|
||||
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
|
||||
return super().action_generate_lot_line_vals(context, mode, first_lot, count, lot_text)
|
||||
24
models/stock_move_line.py
Normal file
24
models/stock_move_line.py
Normal file
@ -0,0 +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
|
||||
return vals
|
||||
22
views/product_views.xml
Normal file
22
views/product_views.xml
Normal file
@ -0,0 +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>
|
||||
</odoo>
|
||||
Loading…
Reference in New Issue
Block a user