it_manager/locations: редактор шаблона этикеток, раздельная печать QR/EAN и карточка поставщика.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-07 16:40:48 +03:00
parent 82ad90c661
commit d54f6fbe42
18 changed files with 1192 additions and 66 deletions
+130 -6
View File
@@ -11,9 +11,21 @@ from barcode.writer import ImageWriter
from PIL import Image
# Внутренний префикс EAN (2 = локальное использование) + 00 → 200…
_EAN_PREFIX = '200'
TEXT_BLOCK_TYPES = frozenset({'model', 'type', 'inventory', 'serial', 'custom'})
CODE_BLOCK_TYPES = frozenset({'qr', 'ean13'})
def parse_label_codes(raw: str | None) -> str:
"""Нормализация: qr | ean | both."""
value = (raw or 'both').strip().lower()
if value in ('qr', 'ean', 'both'):
return value
if value in ('ean13', 'barcode'):
return 'ean'
return 'both'
def equipment_ean13_body(pk: int) -> str:
"""12 цифр без контрольной: 200 + pk (9 знаков)."""
@@ -25,7 +37,6 @@ def equipment_ean13_body(pk: int) -> str:
def equipment_ean13(pk: int) -> str:
"""Полный EAN-13 (13 цифр) для единицы оборудования."""
EAN13 = get_barcode_class('ean13')
# python-barcode сам считает check digit из 12 цифр
code = EAN13(equipment_ean13_body(pk), writer=ImageWriter())
return code.get_fullcode()
@@ -78,18 +89,131 @@ def ean13_png_base64(pk: int) -> tuple[str, str]:
return _png_b64(img), code.get_fullcode()
def label_payload_for_unit(unit) -> dict[str, Any]:
def get_default_template():
from apps.it_manager.models import EquipmentLabelTemplate
return EquipmentLabelTemplate.get_default()
def _block_visible(block_type: str, codes: str) -> bool:
if block_type in TEXT_BLOCK_TYPES:
return True
if block_type == 'qr':
return codes in ('qr', 'both')
if block_type == 'ean13':
return codes in ('ean', 'both')
return False
def _text_for_block(block_type: str, block: dict, fields: dict[str, str]) -> str:
if block_type == 'custom':
return (block.get('text') or '').strip()
if block_type == 'model':
return fields['model']
if block_type == 'type':
return fields['type_name']
if block_type == 'inventory':
return fields['inventory'] or f"EQ{fields['id']}"
if block_type == 'serial':
serial = fields['serial']
return f'S/N {serial}' if serial else ''
return ''
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()
qr_text = equipment_qr_payload(unit)
ean_b64, ean_digits = ean13_png_base64(unit.pk)
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 = ('', '')
if need_ean:
ean_b64, ean_digits = ean13_png_base64(unit.pk)
model = unit.equipment_model
return {
fields = {
'id': unit.pk,
'model': str(model) if model else '—',
'type_name': model.equipment_type.name if model and model.equipment_type_id else '',
'serial': unit.serial_number or '',
'inventory': (unit.inventory_number or '').strip(),
}
blocks_out = []
for raw in (template.layout or []):
if not isinstance(raw, dict):
continue
btype = (raw.get('type') or '').strip()
if not _block_visible(btype, codes):
continue
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),
'bold': bool(raw.get('bold')),
'align': (raw.get('align') or 'left'),
'text': '',
'png': '',
}
if btype == 'qr':
block['png'] = qr_png
block['text'] = qr_text
elif btype == 'ean13':
block['png'] = ean_b64
block['text'] = ean_digits
else:
block['text'] = _text_for_block(btype, raw, fields)
blocks_out.append(block)
return {
'id': unit.pk,
'model': fields['model'],
'type_name': fields['type_name'],
'serial': fields['serial'],
'inventory': fields['inventory'],
'qr_payload': qr_text,
'qr_png': qr_png_base64(qr_text),
'qr_png': qr_png,
'ean13': ean_digits,
'ean_png': ean_b64,
'codes': codes,
'width_mm': float(template.width_mm),
'height_mm': float(template.height_mm),
'blocks': blocks_out,
}
def normalize_layout(raw_blocks) -> list[dict]:
"""Валидация layout из редактора."""
allowed = CODE_BLOCK_TYPES | TEXT_BLOCK_TYPES
out = []
if not isinstance(raw_blocks, list):
return out
for i, raw in enumerate(raw_blocks):
if not isinstance(raw, dict):
continue
btype = (raw.get('type') or '').strip()
if btype not in allowed:
continue
bid = (raw.get('id') or f'{btype}-{i}').strip()[:40]
out.append({
'id': bid,
'type': btype,
'x_mm': max(0.0, float(raw.get('x_mm') or 0)),
'y_mm': max(0.0, float(raw.get('y_mm') or 0)),
'w_mm': max(2.0, float(raw.get('w_mm') or 10)),
'h_mm': max(2.0, float(raw.get('h_mm') or 10)),
'font_pt': max(6.0, min(24.0, float(raw.get('font_pt') or 9))),
'bold': bool(raw.get('bold')),
'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 '',
})
return out