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:
parent
57e87b4692
commit
5a4a65c65b
11
check_stock_lot_fields.py
Normal file
11
check_stock_lot_fields.py
Normal 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}")
|
Binary file not shown.
|
@ -1,11 +1,21 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from odoo import models, fields, api
|
from odoo import models, fields, api
|
||||||
|
from datetime import datetime
|
||||||
|
import random
|
||||||
|
|
||||||
class StockLot(models.Model):
|
class StockLot(models.Model):
|
||||||
_inherit = 'stock.lot'
|
_inherit = 'stock.lot'
|
||||||
|
|
||||||
is_lab_sample = fields.Boolean(string='Is a Laboratory Sample')
|
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(
|
patient_id = fields.Many2one(
|
||||||
'res.partner',
|
'res.partner',
|
||||||
string='Patient',
|
string='Patient',
|
||||||
|
@ -112,3 +122,96 @@ class StockLot(models.Model):
|
||||||
elif self.container_type:
|
elif self.container_type:
|
||||||
return dict(self._fields['container_type'].selection).get(self.container_type)
|
return dict(self._fields['container_type'].selection).get(self.container_type)
|
||||||
return 'Unknown'
|
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
|
||||||
|
|
Loading…
Reference in New Issue
Block a user