From 38b3844e61be396cca4c47c5fbfd4a788ecd230e Mon Sep 17 00:00:00 2001 From: "admin.suherdy" Date: Wed, 12 Nov 2025 15:57:46 +0700 Subject: [PATCH] fix for odoo 19 --- README.md | 82 +++++----- __manifest__.py | 26 ++-- __pycache__/__init__.cpython-310.pyc | Bin 0 -> 231 bytes models/__init__.py | 8 +- models/__pycache__/__init__.cpython-310.pyc | Bin 0 -> 388 bytes .../mrp_production.cpython-310.pyc | Bin 0 -> 1430 bytes .../product_template.cpython-310.pyc | Bin 0 -> 2522 bytes models/__pycache__/stock_lot.cpython-310.pyc | Bin 0 -> 949 bytes models/__pycache__/stock_move.cpython-310.pyc | Bin 0 -> 2816 bytes .../stock_move_line.cpython-310.pyc | Bin 0 -> 1139 bytes models/mrp_production.py | 96 ++++++------ models/product_template.py | 124 +++++++-------- models/stock_lot.py | 36 ++--- models/stock_move.py | 141 +++++++++--------- models/stock_move_line.py | 46 +++--- models/stock_quant.py | 76 +++++----- views/product_views.xml | 42 +++--- 17 files changed, 339 insertions(+), 338 deletions(-) create mode 100644 __pycache__/__init__.cpython-310.pyc create mode 100644 models/__pycache__/__init__.cpython-310.pyc create mode 100644 models/__pycache__/mrp_production.cpython-310.pyc create mode 100644 models/__pycache__/product_template.cpython-310.pyc create mode 100644 models/__pycache__/stock_lot.cpython-310.pyc create mode 100644 models/__pycache__/stock_move.cpython-310.pyc create mode 100644 models/__pycache__/stock_move_line.cpython-310.pyc diff --git a/README.md b/README.md index 7cc2dc7..ba36916 100644 --- a/README.md +++ b/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 \ No newline at end of file diff --git a/__manifest__.py b/__manifest__.py index 10f11a8..fba1d44 100644 --- a/__manifest__.py +++ b/__manifest__.py @@ -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, } \ No newline at end of file diff --git a/__pycache__/__init__.cpython-310.pyc b/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..026e5af0bb3adb3b3194013a7ab61e44cc36dfd0 GIT binary patch literal 231 zcmYjLy9&ZU5WI^bA_x}#M4B9S79xIu2zE9{yj@5zcN_0fk&O2MSr7H{-HK;=uD{_W^C<3C@TA36g*%E+UP^~9OChCTw j4tP!I4C)rW{vDLowX37nMceh$4R=WmZii}N=&}$W*&ROb literal 0 HcmV?d00001 diff --git a/models/__init__.py b/models/__init__.py index e66ea8a..23e3486 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -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 \ No newline at end of file diff --git a/models/__pycache__/__init__.cpython-310.pyc b/models/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..86288cc46dcf782bfe1a4bd8104bf08f2bf26337 GIT binary patch literal 388 zcmYk2%}&H16osKb<2aeod5vyL+_^Ajp1_#6>xNVyX(MneP?Nrd@8K(T>&mUVa^=0% ziC)6tI}mOTz;3r;SRbG7-7~JQWd2u%=72MjXr{SkjyIeWXUJKTEu16gO}=n}Tr|bP zC34x6$g4Y6oxE{uQ}KB}1kj^OG8s46HW}R8#02U=I(Q;?`r|AlH0OUu()OOt&s-Sf zZYhseGAUbeH#IZ9$PxaI)xIegdXnn1q?zOm`QU_jv zHUc!j#idXq25_y`;G;N3$7nK|$I*CYVI`y^#OO!N8*H%Du(>HU$u Le9N|+#-4uz-D_$4 literal 0 HcmV?d00001 diff --git a/models/__pycache__/mrp_production.cpython-310.pyc b/models/__pycache__/mrp_production.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d2b64876cf7b889289c1b500c976cb7623f9f5ef GIT binary patch literal 1430 zcma)6&5k2A5Vqa!pY(JlSqULVTq41Of>0umR@wugk$~m2qk$EdNXY7GJChFmH@16} zM4by+j`Ic_k|VEVapN>^fJU&Q+%s8a4oEn1#Z}e*>Z>Z-Ww#q57~-!#X39tCPpcdb z3d$)=^*IQRIL=UxCpae)f~`(6C#MsN(RYYDoW4e!3Osi=&cx&HD>U?OVILY2@QYO}YI0z`GFx7h?1e#!uCWPaO!wGj@qY32{U|rLFDXVi?@;cFJ zS*(t8S&gc&le<6=R8d}7;xV4>uBj9Ffp6b<6 zDo1<2Gdk3J$ThR2WNGDA>^QxgLuHjG_ErZAKp_P z5*a`s8QExk6N*CBQn0cRt=;tYv~^x(z|;7bLdRN5<3KFq%DA``CM?84vkRMuabgZa z6xYU6B9_U_5J<00EZTpTz!^7v22iXu-De@~C6bP1(lJztY}(Q?{sRX?EeRX=%D7sl zf*Zn8(@Eljr##lebi^W6nnAsPNAd#zx?7BAWiH05o(aj9qh*|w7pHj)RUakgDCOhx zrJj|AB{VL1S$_6>oYYX>9I_}2HQuNHsJ)et?7=aN)a~Ow_zpeqfCms9 zg1tAZj~`w4(SLF{gQp>2%$_vHOvo7IQD=4?F?LnQ*@GuwX#u56wR{SM+nOGT53S7w zVW;vy1PI8G)^;}Z4Yf6u_N{9BYI;^5iA5qR`y8pZ<;Hz!-@JCy0l4j-v=L13#XY;P O?44-C^ihQ0b^Zm!KZY>? literal 0 HcmV?d00001 diff --git a/models/__pycache__/product_template.cpython-310.pyc b/models/__pycache__/product_template.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..04d2d22bf6b2da925b2885d7dc6b30cc79e8e31d GIT binary patch literal 2522 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 literal 0 HcmV?d00001 diff --git a/models/mrp_production.py b/models/mrp_production.py index da4637a..fce220a 100644 --- a/models/mrp_production.py +++ b/models/mrp_production.py @@ -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, } \ No newline at end of file diff --git a/models/product_template.py b/models/product_template.py index c1187c7..30b44a6 100644 --- a/models/product_template.py +++ b/models/product_template.py @@ -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' \ No newline at end of file diff --git a/models/stock_lot.py b/models/stock_lot.py index cbbc509..68b6019 100644 --- a/models/stock_lot.py +++ b/models/stock_lot.py @@ -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) \ No newline at end of file diff --git a/models/stock_move.py b/models/stock_move.py index 7707655..e713ed8 100644 --- a/models/stock_move.py +++ b/models/stock_move.py @@ -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) \ No newline at end of file diff --git a/models/stock_move_line.py b/models/stock_move_line.py index 559b469..af26337 100644 --- a/models/stock_move_line.py +++ b/models/stock_move_line.py @@ -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 \ No newline at end of file diff --git a/models/stock_quant.py b/models/stock_quant.py index 1f13d57..f638e1e 100644 --- a/models/stock_quant.py +++ b/models/stock_quant.py @@ -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 \ No newline at end of file diff --git a/views/product_views.xml b/views/product_views.xml index 759f667..8eb091a 100644 --- a/views/product_views.xml +++ b/views/product_views.xml @@ -1,22 +1,22 @@ - - - - - product.template.form.inherit.stock.lot.sequence - product.template - - - - - - - - + + + + + product.template.form.inherit.stock.lot.sequence + product.template + + + + + + + + \ No newline at end of file