diff --git a/apps/it_manager/equipment_labels.py b/apps/it_manager/equipment_labels.py index d73a4f2..f802ae0 100644 --- a/apps/it_manager/equipment_labels.py +++ b/apps/it_manager/equipment_labels.py @@ -41,6 +41,11 @@ def equipment_ean13(pk: int) -> str: return code.get_fullcode() +def suggested_inventory_number(pk: int) -> str: + """Автоинв. № = EAN-13 единицы (удобно для скана и QR).""" + return equipment_ean13(pk) + + def equipment_qr_payload(unit) -> str: """Что кодируем в QR: инв. №, иначе EQ{pk}.""" inv = (getattr(unit, 'inventory_number', None) or '').strip() @@ -149,14 +154,25 @@ def label_payload_for_unit(unit, *, codes: str = 'both', template=None) -> dict[ 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) + font_pt = float(raw.get('font_pt') or 9) + # CSS mm: точка, не запятая локали (иначе layout «не применяется») block = { 'id': raw.get('id') or btype, 'type': btype, - '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), - 'font_pt': float(raw.get('font_pt') or 9), + 'x_mm': x_mm, + 'y_mm': y_mm, + 'w_mm': w_mm, + 'h_mm': h_mm, + 'font_pt': font_pt, + 'x_css': f'{x_mm:.2f}', + 'y_css': f'{y_mm:.2f}', + 'w_css': f'{w_mm:.2f}', + 'h_css': f'{h_mm:.2f}', + 'font_css': f'{font_pt:.1f}', 'bold': bool(raw.get('bold')), 'align': (raw.get('align') or 'left'), 'text': '', @@ -185,6 +201,8 @@ def label_payload_for_unit(unit, *, codes: str = 'both', template=None) -> dict[ '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}', 'blocks': blocks_out, } diff --git a/apps/it_manager/tests.py b/apps/it_manager/tests.py index 24e0620..4e8522e 100644 --- a/apps/it_manager/tests.py +++ b/apps/it_manager/tests.py @@ -441,6 +441,9 @@ class ItManagerEquipmentTests(TestCase): self.assertContains(r, 'INV-LBL-1') self.assertContains(r, 'data:image/png;base64,') self.assertContains(r, ean) + # позиции в CSS с точкой (не запятой локали) + self.assertContains(r, 'left:') + self.assertRegex(r.content.decode('utf-8'), r'left:\d+\.\d+mm') r_qr = self.client.get(f'/it-manager/equipment/labels/?ids={eq.pk}&codes=qr') self.assertEqual(r_qr.status_code, 200) @@ -457,6 +460,19 @@ class ItManagerEquipmentTests(TestCase): serials = [e.serial_number for e in r2.context['equipment_page']] self.assertIn('LBL-SN-1', serials) + def test_auto_inventory_on_bulk_create(self): + from apps.it_manager.equipment_labels import suggested_inventory_number + + model = make_equipment_model() + r = self.client.post('/it-manager/equipment/bulk-create/', { + 'equipment_model_id': str(model.pk), + 'serial_numbers': 'SN-AUTO-INV-1', + 'auto_inventory': '1', + }, HTTP_HX_REQUEST='true') + self.assertEqual(r.status_code, 200) + eq = Equipment.objects.get(serial_number='SN-AUTO-INV-1') + self.assertEqual(eq.inventory_number, suggested_inventory_number(eq.pk)) + def test_label_template_editor(self): import json diff --git a/apps/it_manager/views.py b/apps/it_manager/views.py index 45a9a3a..40676cf 100644 --- a/apps/it_manager/views.py +++ b/apps/it_manager/views.py @@ -640,6 +640,7 @@ def equipment_bulk_create_view(request): while inv_lines and not inv_lines[-1].strip(): inv_lines.pop() inventory_list = [(line.strip() or None) for line in inv_lines] + auto_inventory = bool(request.POST.get('auto_inventory')) model = EquipmentModel.objects.filter(pk=model_id).first() if model_id else None if not model: @@ -705,6 +706,8 @@ def equipment_bulk_create_view(request): scoped_queryset(request.user, Equipment.objects.all()).filter(serial_number__in=serials) .values_list('serial_number', flat=True) ) + from apps.it_manager.equipment_labels import suggested_inventory_number + created = 0 for idx, s in enumerate(serials): if s in existing: @@ -729,6 +732,13 @@ def equipment_bulk_create_view(request): context['it_active_tab'] = 'equipment' context.update(context_extra) return render_page(request, 'it-manager.html', context) + if not unit.inventory_number and auto_inventory: + unit.inventory_number = suggested_inventory_number(unit.pk) + try: + unit.save(update_fields=['inventory_number']) + except IntegrityError: + unit.inventory_number = None + unit.save(update_fields=['inventory_number']) if location_id or assigned_to_id: EquipmentAssignment.objects.create( equipment=unit, @@ -900,6 +910,9 @@ def equipment_update_view(request, pk): new_status_id = _resolve_directory('equipment_status', new_status_code) target, location_id, assigned_to_id, assign_err = _parse_assign_post(request) inventory_number = (request.POST.get('inventory_number') or '').strip() or None + if not inventory_number and request.POST.get('auto_inventory'): + from apps.it_manager.equipment_labels import suggested_inventory_number + inventory_number = suggested_inventory_number(equipment.pk) notes = request.POST.get('notes', '') purchase_date = request.POST.get('purchase_date') or None warranty_until = request.POST.get('warranty_until') or None diff --git a/static/assets/js/it-manager.js b/static/assets/js/it-manager.js index 997921f..eb6cb5e 100644 --- a/static/assets/js/it-manager.js +++ b/static/assets/js/it-manager.js @@ -160,6 +160,27 @@ function itEquipmentOpenLabels() { window.open(url, '_blank', 'noopener'); } +/** EAN-13 check digit + тело 200 + pk(9) — как на сервере. */ +function itEquipmentEan13(pk) { + var body = '200' + String(pk).padStart(9, '0'); + var sum = 0; + for (var i = 0; i < 12; i++) { + var d = parseInt(body.charAt(i), 10); + sum += (i % 2 === 0) ? d : d * 3; + } + var check = (10 - (sum % 10)) % 10; + return body + String(check); +} + +function itFillInventoryEan(btn, pk) { + var input = document.getElementById('it-inv-' + pk); + var flag = document.getElementById('it-auto-inv-' + pk); + if (!input) return; + input.value = itEquipmentEan13(pk); + if (flag) flag.value = '1'; + input.focus(); +} + function itSetSearchableSelectValue(selectId, value) { var select = document.getElementById(selectId); if (!select) return; diff --git a/templates/pages/it-equipment-labels.html b/templates/pages/it-equipment-labels.html index b987ce7..96b9b60 100644 --- a/templates/pages/it-equipment-labels.html +++ b/templates/pages/it-equipment-labels.html @@ -36,8 +36,6 @@ } .label { position: relative; - width: {{ template_width_mm }}mm; - height: {{ template_height_mm }}mm; border: 1px solid #222; border-radius: 1mm; overflow: hidden; @@ -72,23 +70,27 @@ {% trans "Этикеток" %}: {{ labels|length }} · - {% trans "по шаблону" %} {{ template_width_mm }}×{{ template_height_mm }} {% trans "мм" %} + {% trans "по шаблону" %} + {% if labels %}{{ labels.0.width_css }}×{{ labels.0.height_css }}{% else %}{{ template_width_mm }}×{{ template_height_mm }}{% endif %} + {% trans "мм" %} {% trans "Редактировать шаблон" %} {% trans "К оборудованию" %}
{% endif %}
{% else %}
{{ block.text }}
{% endif %}