feat(#32): Enhanced barcode generation with uniqueness - Task 3 completed

- Added barcode field to stock.lot with automatic generation
- Implemented unique barcode generation in format YYMMDDNNNNNNC
- Added Luhn check digit for barcode validation
- Handles high volume scenarios with sample type prefixes
- Collision detection and retry mechanism for uniqueness
- Successful test with ephemeral instance restart
This commit is contained in:
Luis Ernesto Portillo Zaldivar 2025-07-14 22:38:18 -06:00
parent 57e87b4692
commit 5a4a65c65b
3 changed files with 114 additions and 0 deletions

11
check_stock_lot_fields.py Normal file
View File

@ -0,0 +1,11 @@
import odoo
db_name = 'lims_demo'
registry = odoo.registry(db_name)
with registry.cursor() as cr:
env = odoo.api.Environment(cr, 1, {})
fields = env['stock.lot']._fields.keys()
print("Stock.lot fields containing 'name' or 'barcode':")
for f in fields:
if 'name' in f or 'barcode' in f:
print(f" - {f}")

View File

@ -1,11 +1,21 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api
from datetime import datetime
import random
class StockLot(models.Model):
_inherit = 'stock.lot'
is_lab_sample = fields.Boolean(string='Is a Laboratory Sample')
barcode = fields.Char(
string='Código de Barras',
compute='_compute_barcode',
store=True,
readonly=True,
help="Código de barras único para la muestra en formato YYMMDDNNNNNNC"
)
patient_id = fields.Many2one(
'res.partner',
string='Patient',
@ -112,3 +122,96 @@ class StockLot(models.Model):
elif self.container_type:
return dict(self._fields['container_type'].selection).get(self.container_type)
return 'Unknown'
@api.depends('is_lab_sample', 'create_date')
def _compute_barcode(self):
"""Generate unique barcode for laboratory samples"""
for record in self:
if record.is_lab_sample and not record.barcode:
record.barcode = record._generate_unique_barcode()
elif not record.is_lab_sample:
record.barcode = False
def _generate_unique_barcode(self):
"""Generate a unique barcode in format YYMMDDNNNNNNC
YY: Year (2 digits)
MM: Month (2 digits)
DD: Day (2 digits)
NNNNNN: Sequential number (6 digits)
C: Check digit
"""
self.ensure_one()
now = datetime.now()
date_prefix = now.strftime('%y%m%d')
# Get the highest sequence number for today
domain = [
('is_lab_sample', '=', True),
('barcode', 'like', date_prefix + '%'),
('id', '!=', self.id)
]
max_barcode = self.search(domain, order='barcode desc', limit=1)
if max_barcode and max_barcode.barcode:
# Extract sequence number from existing barcode
try:
sequence = int(max_barcode.barcode[6:12]) + 1
except:
sequence = 1
else:
sequence = 1
# Ensure we don't exceed 6 digits
if sequence > 999999:
# Add prefix based on sample type to allow more barcodes
prefix_map = {
'suero': '1',
'edta': '2',
'orina': '3',
'hisopo': '4',
'other': '9'
}
type_prefix = '9' # default
if self.sample_type_product_id:
name_lower = self.sample_type_product_id.name.lower()
for key, val in prefix_map.items():
if key in name_lower:
type_prefix = val
break
sequence = int(type_prefix + str(sequence % 100000).zfill(5))
# Format sequence with leading zeros
sequence_str = str(sequence).zfill(6)
# Calculate check digit using Luhn algorithm
barcode_without_check = date_prefix + sequence_str
check_digit = self._calculate_luhn_check_digit(barcode_without_check)
final_barcode = barcode_without_check + str(check_digit)
# Verify uniqueness
existing = self.search([
('barcode', '=', final_barcode),
('id', '!=', self.id)
], limit=1)
if existing:
# If collision, add random component and retry
sequence = sequence * 10 + random.randint(0, 9)
sequence_str = str(sequence % 1000000).zfill(6)
barcode_without_check = date_prefix + sequence_str
check_digit = self._calculate_luhn_check_digit(barcode_without_check)
final_barcode = barcode_without_check + str(check_digit)
return final_barcode
def _calculate_luhn_check_digit(self, number_str):
"""Calculate Luhn check digit for barcode validation"""
digits = [int(d) for d in number_str]
odd_sum = sum(digits[-1::-2])
even_sum = sum([sum(divmod(2 * d, 10)) for d in digits[-2::-2]])
total = odd_sum + even_sum
return (10 - (total % 10)) % 10