commit 87b5351da2edea766e5ed3672bd0b0fed92f44f8 Author: Suherdy SYC. Yacob Date: Mon Sep 29 11:47:29 2025 +0700 first commit diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..9a7e03e --- /dev/null +++ b/__init__.py @@ -0,0 +1 @@ +from . import models \ No newline at end of file diff --git a/__manifest__.py b/__manifest__.py new file mode 100644 index 0000000..10f11a8 --- /dev/null +++ b/__manifest__.py @@ -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, +} \ No newline at end of file diff --git a/__pycache__/__init__.cpython-312.pyc b/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..b5e3dca Binary files /dev/null and b/__pycache__/__init__.cpython-312.pyc differ diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..e66ea8a --- /dev/null +++ b/models/__init__.py @@ -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 \ No newline at end of file diff --git a/models/__pycache__/__init__.cpython-312.pyc b/models/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..a7341e9 Binary files /dev/null and b/models/__pycache__/__init__.cpython-312.pyc differ diff --git a/models/__pycache__/mrp_production.cpython-312.pyc b/models/__pycache__/mrp_production.cpython-312.pyc new file mode 100644 index 0000000..e83d3f5 Binary files /dev/null and b/models/__pycache__/mrp_production.cpython-312.pyc differ diff --git a/models/__pycache__/product_template.cpython-312.pyc b/models/__pycache__/product_template.cpython-312.pyc new file mode 100644 index 0000000..bd0e794 Binary files /dev/null and b/models/__pycache__/product_template.cpython-312.pyc differ diff --git a/models/__pycache__/stock_lot.cpython-312.pyc b/models/__pycache__/stock_lot.cpython-312.pyc new file mode 100644 index 0000000..1d02112 Binary files /dev/null and b/models/__pycache__/stock_lot.cpython-312.pyc differ diff --git a/models/__pycache__/stock_move.cpython-312.pyc b/models/__pycache__/stock_move.cpython-312.pyc new file mode 100644 index 0000000..4a68854 Binary files /dev/null and b/models/__pycache__/stock_move.cpython-312.pyc differ diff --git a/models/__pycache__/stock_move_line.cpython-312.pyc b/models/__pycache__/stock_move_line.cpython-312.pyc new file mode 100644 index 0000000..a00a711 Binary files /dev/null and b/models/__pycache__/stock_move_line.cpython-312.pyc differ diff --git a/models/mrp_production.py b/models/mrp_production.py new file mode 100644 index 0000000..da4637a --- /dev/null +++ b/models/mrp_production.py @@ -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, + } \ No newline at end of file diff --git a/models/product_template.py b/models/product_template.py new file mode 100644 index 0000000..c1187c7 --- /dev/null +++ b/models/product_template.py @@ -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' \ No newline at end of file diff --git a/models/stock_lot.py b/models/stock_lot.py new file mode 100644 index 0000000..cbbc509 --- /dev/null +++ b/models/stock_lot.py @@ -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) \ No newline at end of file diff --git a/models/stock_move.py b/models/stock_move.py new file mode 100644 index 0000000..7707655 --- /dev/null +++ b/models/stock_move.py @@ -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) \ No newline at end of file diff --git a/models/stock_move_line.py b/models/stock_move_line.py new file mode 100644 index 0000000..559b469 --- /dev/null +++ b/models/stock_move_line.py @@ -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 \ No newline at end of file diff --git a/views/product_views.xml b/views/product_views.xml new file mode 100644 index 0000000..759f667 --- /dev/null +++ b/views/product_views.xml @@ -0,0 +1,22 @@ + + + + + product.template.form.inherit.stock.lot.sequence + product.template + + + + + + + + + \ No newline at end of file