diff --git a/apps/it_manager/equipment_labels.py b/apps/it_manager/equipment_labels.py index f802ae0..9e20912 100644 --- a/apps/it_manager/equipment_labels.py +++ b/apps/it_manager/equipment_labels.py @@ -124,11 +124,41 @@ def _text_for_block(block_type: str, block: dict, fields: dict[str, str]) -> str return '' +def ensure_layout_has_text(layout: list | None, *, width_mm: float, height_mm: float) -> list: + """Если в шаблоне только QR/EAN — подмешать текстовые блоки из дефолта.""" + from apps.it_manager.models.label_template import DEFAULT_LABEL_LAYOUT + + blocks = [b for b in (layout or []) if isinstance(b, dict)] + present = {(b.get('type') or '').strip() for b in blocks} + if present & TEXT_BLOCK_TYPES: + return blocks + for raw in DEFAULT_LABEL_LAYOUT: + if raw['type'] in TEXT_BLOCK_TYPES and raw['type'] not in present: + block = dict(raw) + # чуть подогнать под текущий размер этикетки + block['x_mm'] = min(float(block['x_mm']), max(0.0, width_mm - float(block['w_mm']))) + block['y_mm'] = min(float(block['y_mm']), max(0.0, height_mm - float(block['h_mm']))) + blocks.append(block) + present.add(raw['type']) + return blocks + + +def _clamp_block_geom(x, y, w, h, *, width_mm: float, height_mm: float): + w = max(2.0, min(float(w), width_mm)) + h = max(2.0, min(float(h), height_mm)) + x = max(0.0, min(float(x), max(0.0, width_mm - w))) + y = max(0.0, min(float(y), max(0.0, height_mm - h))) + return x, y, w, h + + def label_payload_for_unit(unit, *, codes: str = 'both', template=None) -> dict[str, Any]: codes = parse_label_codes(codes) if template is None: template = get_default_template() + 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') @@ -147,17 +177,28 @@ def label_payload_for_unit(unit, *, codes: str = 'both', template=None) -> dict[ 'inventory': (unit.inventory_number or '').strip(), } + layout = ensure_layout_has_text( + template.layout, width_mm=width_mm, height_mm=height_mm, + ) + blocks_out = [] - for raw in (template.layout or []): - if not isinstance(raw, dict): - continue + for raw in layout: btype = (raw.get('type') or '').strip() if not _block_visible(btype, codes): continue - x_mm = float(raw.get('x_mm') or 0) - y_mm = float(raw.get('y_mm') or 0) - w_mm = float(raw.get('w_mm') or 10) - h_mm = float(raw.get('h_mm') or 10) + x_mm, y_mm, w_mm, h_mm = _clamp_block_geom( + raw.get('x_mm') or 0, + raw.get('y_mm') or 0, + raw.get('w_mm') or 10, + raw.get('h_mm') or 10, + width_mm=width_mm, + height_mm=height_mm, + ) + # текстовым блокам — чуть больше высоты, чтобы pt не обрезался overflow + if btype in TEXT_BLOCK_TYPES: + h_mm = max(h_mm, max(4.0, float(raw.get('font_pt') or 9) * 0.45)) + if y_mm + h_mm > height_mm: + y_mm = max(0.0, height_mm - h_mm) font_pt = float(raw.get('font_pt') or 9) # CSS mm: точка, не запятая локали (иначе layout «не применяется») block = { @@ -186,6 +227,8 @@ def label_payload_for_unit(unit, *, codes: str = 'both', template=None) -> dict[ block['text'] = ean_digits else: block['text'] = _text_for_block(btype, raw, fields) + if btype in TEXT_BLOCK_TYPES and not block['text']: + continue blocks_out.append(block) return { @@ -199,10 +242,10 @@ def label_payload_for_unit(unit, *, codes: str = 'both', template=None) -> dict[ 'ean13': ean_digits, 'ean_png': ean_b64, 'codes': codes, - 'width_mm': float(template.width_mm), - 'height_mm': float(template.height_mm), - 'width_css': f'{float(template.width_mm):.2f}', - 'height_css': f'{float(template.height_mm):.2f}', + 'width_mm': width_mm, + 'height_mm': height_mm, + 'width_css': f'{width_mm:.2f}', + 'height_css': f'{height_mm:.2f}', 'blocks': blocks_out, } diff --git a/apps/it_manager/tests.py b/apps/it_manager/tests.py index 4e8522e..b3ca46a 100644 --- a/apps/it_manager/tests.py +++ b/apps/it_manager/tests.py @@ -473,6 +473,40 @@ 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_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 + + tmpl = EquipmentLabelTemplate.get_default() + tmpl.layout = [ + { + 'id': 'qr', 'type': 'qr', + 'x_mm': 2, 'y_mm': 2, 'w_mm': 20, 'h_mm': 20, + 'font_pt': 9, 'bold': False, 'align': 'left', 'text': '', + }, + { + 'id': 'ean', 'type': 'ean13', + 'x_mm': 2, 'y_mm': 24, 'w_mm': 60, 'h_mm': 14, + 'font_pt': 8, 'bold': False, 'align': 'center', 'text': '', + }, + ] + tmpl.save(update_fields=['layout']) + + eq = make_equipment(serial_number='TXT-SN-1', inventory_number='INV-TXT-1') + payload = label_payload_for_unit(eq, template=tmpl) + types = {b['type'] for b in payload['blocks']} + self.assertIn('qr', types) + self.assertIn('ean13', types) + self.assertIn('model', types) + self.assertIn('inventory', types) + inv_block = next(b for b in payload['blocks'] if b['type'] == 'inventory') + self.assertEqual(inv_block['text'], 'INV-TXT-1') + + r = self.client.get(f'/it-manager/equipment/labels/?ids={eq.pk}') + self.assertEqual(r.status_code, 200) + self.assertContains(r, 'INV-TXT-1') + self.assertContains(r, 'block text') + def test_label_template_editor(self): import json diff --git a/templates/pages/it-equipment-labels.html b/templates/pages/it-equipment-labels.html index 96b9b60..a4ed986 100644 --- a/templates/pages/it-equipment-labels.html +++ b/templates/pages/it-equipment-labels.html @@ -9,9 +9,11 @@ * { box-sizing: border-box; } body { margin: 0; - font-family: system-ui, -apple-system, "Segoe UI", sans-serif; - color: #111; + font-family: system-ui, -apple-system, "Segoe UI", "Noto Sans", sans-serif; + color: #000; background: #fff; + -webkit-print-color-adjust: exact; + print-color-adjust: exact; } .toolbar { position: sticky; top: 0; z-index: 10; @@ -42,26 +44,35 @@ break-inside: avoid; page-break-inside: avoid; background: #fff; + color: #000; } .block { position: absolute; - overflow: hidden; - line-height: 1.15; + line-height: 1.2; + z-index: 1; } - .block img { + .block.code { + overflow: hidden; + } + .block.code img { width: 100%; height: 100%; object-fit: contain; display: block; } .block.text { - white-space: pre-wrap; - word-break: break-word; + z-index: 2; + overflow: visible; + color: #000 !important; + white-space: nowrap; + text-overflow: ellipsis; + padding: 0; } @media print { .toolbar { display: none !important; } .sheet { padding: 0; gap: 4mm; } .label { border-color: #000; } + .block.text { color: #000 !important; } } @@ -81,14 +92,15 @@ {% for label in labels %}
{% endif %}
- {% else %}
- {{ block.text }}
- {% endif %}
+ {% if block.type == 'qr' or block.type == 'ean13' %}
+
{% endif %}