diff --git a/apps/it_manager/equipment_labels.py b/apps/it_manager/equipment_labels.py index 9e20912..827d777 100644 --- a/apps/it_manager/equipment_labels.py +++ b/apps/it_manager/equipment_labels.py @@ -46,12 +46,47 @@ def suggested_inventory_number(pk: int) -> str: return equipment_ean13(pk) -def equipment_qr_payload(unit) -> str: - """Что кодируем в QR: инв. №, иначе EQ{pk}.""" +def _qr_field_map(unit, *, ean13: str = '') -> dict[str, str]: inv = (getattr(unit, 'inventory_number', None) or '').strip() - if inv: - return inv - return f'EQ{unit.pk}' + model = getattr(unit, 'equipment_model', None) + type_name = '' + if model is not None and getattr(model, 'equipment_type_id', None): + type_name = model.equipment_type.name if model.equipment_type_id else '' + return { + 'id': str(unit.pk), + 'eq': f'EQ{unit.pk}', + 'inventory': inv, + 'serial': (unit.serial_number or '').strip(), + 'model': str(model) if model else '', + 'type': type_name, + 'ean13': ean13 or equipment_ean13(unit.pk), + } + + +def render_qr_pattern(pattern: str, fields: dict[str, str]) -> str: + """Подстановка плейсхолдеров {inventory} {serial} {model} {type} {id} {eq} {ean13}.""" + import re + + text = pattern or '' + + def repl(match): + key = (match.group(1) or '').strip().lower() + return fields.get(key, '') + + return re.sub(r'\{([a-zA-Z0-9_]+)\}', repl, text).strip() + + +def equipment_qr_payload(unit, pattern: str | None = None, *, ean13: str = '') -> str: + """Содержимое QR: шаблон из блока или по умолчанию инв. № / EQ{id}.""" + fields = _qr_field_map(unit, ean13=ean13) + pattern = (pattern or '').strip() + if pattern: + rendered = render_qr_pattern(pattern, fields) + if rendered: + return rendered + if fields['inventory']: + return fields['inventory'] + return fields['eq'] def _png_b64(img: Image.Image) -> str: @@ -159,12 +194,11 @@ def label_payload_for_unit(unit, *, codes: str = 'both', template=None) -> dict[ width_mm = float(template.width_mm or 70) height_mm = float(template.height_mm or 42) - qr_text = equipment_qr_payload(unit) need_qr = codes in ('qr', 'both') need_ean = codes in ('ean', 'both') - qr_png = qr_png_base64(qr_text) if need_qr else '' - ean_b64, ean_digits = ('', '') + ean_digits = equipment_ean13(unit.pk) + ean_b64 = '' if need_ean: ean_b64, ean_digits = ean13_png_base64(unit.pk) @@ -176,6 +210,9 @@ def label_payload_for_unit(unit, *, codes: str = 'both', template=None) -> dict[ 'serial': unit.serial_number or '', 'inventory': (unit.inventory_number or '').strip(), } + default_qr = equipment_qr_payload(unit, ean13=ean_digits) + # общий png только как fallback для поиска; каждый QR-блок рисуется со своим pattern + qr_png = qr_png_base64(default_qr) if need_qr else '' layout = ensure_layout_has_text( template.layout, width_mm=width_mm, height_mm=height_mm, @@ -220,8 +257,10 @@ def label_payload_for_unit(unit, *, codes: str = 'both', template=None) -> dict[ 'png': '', } if btype == 'qr': - block['png'] = qr_png - block['text'] = qr_text + pattern = (raw.get('text') or '').strip() + payload = equipment_qr_payload(unit, pattern, ean13=ean_digits) + block['png'] = qr_png_base64(payload) if need_qr else '' + block['text'] = payload elif btype == 'ean13': block['png'] = ean_b64 block['text'] = ean_digits @@ -237,7 +276,7 @@ def label_payload_for_unit(unit, *, codes: str = 'both', template=None) -> dict[ 'type_name': fields['type_name'], 'serial': fields['serial'], 'inventory': fields['inventory'], - 'qr_payload': qr_text, + 'qr_payload': default_qr, 'qr_png': qr_png, 'ean13': ean_digits, 'ean_png': ean_b64, @@ -275,6 +314,10 @@ def normalize_layout(raw_blocks) -> list[dict]: 'align': (raw.get('align') or 'left') if (raw.get('align') or 'left') in ( 'left', 'center', 'right', ) else 'left', - 'text': (raw.get('text') or '')[:200] if btype == 'custom' else '', + 'text': ( + (raw.get('text') or '')[:500] + if btype in ('custom', 'qr') + else '' + ), }) return out diff --git a/apps/it_manager/tests.py b/apps/it_manager/tests.py index b3ca46a..b13d3bf 100644 --- a/apps/it_manager/tests.py +++ b/apps/it_manager/tests.py @@ -473,6 +473,34 @@ class ItManagerEquipmentTests(TestCase): eq = Equipment.objects.get(serial_number='SN-AUTO-INV-1') self.assertEqual(eq.inventory_number, suggested_inventory_number(eq.pk)) + def test_qr_custom_pattern(self): + from apps.it_manager.equipment_labels import equipment_qr_payload, label_payload_for_unit + from apps.it_manager.models import EquipmentLabelTemplate + + eq = make_equipment(serial_number='QR-SN-9', inventory_number='INV-QR-9') + self.assertEqual(equipment_qr_payload(eq), 'INV-QR-9') + self.assertEqual( + equipment_qr_payload(eq, 'ACORE|{serial}|{inventory}'), + 'ACORE|QR-SN-9|INV-QR-9', + ) + self.assertEqual(equipment_qr_payload(eq, 'id={id}'), f'id={eq.pk}') + + tmpl = EquipmentLabelTemplate.get_default() + tmpl.layout = [ + { + 'id': 'qr1', 'type': 'qr', + 'x_mm': 2, 'y_mm': 2, 'w_mm': 22, 'h_mm': 22, + 'font_pt': 9, 'bold': False, 'align': 'left', + 'text': 'S:{serial}', + }, + ] + tmpl.width_mm = 40 + tmpl.height_mm = 30 + tmpl.save() + payload = label_payload_for_unit(eq, template=tmpl) + qr_blocks = [b for b in payload['blocks'] if b['type'] == 'qr'] + self.assertEqual(qr_blocks[0]['text'], 'S:QR-SN-9') + def test_labels_inject_text_when_template_codes_only(self): from apps.it_manager.equipment_labels import label_payload_for_unit from apps.it_manager.models import EquipmentLabelTemplate diff --git a/static/assets/js/label-template-editor.js b/static/assets/js/label-template-editor.js index ec9b102..81f88ea 100644 --- a/static/assets/js/label-template-editor.js +++ b/static/assets/js/label-template-editor.js @@ -73,6 +73,11 @@ function previewText(block) { if (block.type === 'custom') return block.text || 'Текст'; + if (block.type === 'qr') { + var p = (block.text || '').trim(); + if (!p) return 'инв. / EQ{id}'; + return p.length > 28 ? p.slice(0, 28) + '…' : p; + } return DEMO[block.type] || block.type; } @@ -141,7 +146,14 @@ document.getElementById('prop-align').value = block.align || 'left'; document.getElementById('prop-bold').checked = !!block.bold; var customWrap = document.getElementById('prop-custom-wrap'); - customWrap.style.display = block.type === 'custom' ? '' : 'none'; + var qrHints = document.getElementById('prop-qr-hints'); + var textLabel = document.getElementById('prop-text-label'); + var showText = block.type === 'custom' || block.type === 'qr'; + customWrap.style.display = showText ? '' : 'none'; + if (qrHints) qrHints.style.display = block.type === 'qr' ? '' : 'none'; + if (textLabel) { + textLabel.textContent = block.type === 'qr' ? 'Содержимое QR' : 'Текст'; + } document.getElementById('prop-text').value = block.text || ''; } @@ -160,7 +172,7 @@ function addBlock(type) { var defaults = { - qr: { w_mm: 22, h_mm: 22 }, + qr: { w_mm: 22, h_mm: 22, text: '{inventory}' }, ean13: { w_mm: 50, h_mm: 14 }, model: { w_mm: 35, h_mm: 6, bold: true, font_pt: 10 }, type: { w_mm: 35, h_mm: 5, font_pt: 8 }, diff --git a/templates/pages/it-equipment-label-template.html b/templates/pages/it-equipment-label-template.html index 158fd55..3957f05 100644 --- a/templates/pages/it-equipment-label-template.html +++ b/templates/pages/it-equipment-label-template.html @@ -151,8 +151,21 @@