Files
ACORE/apps/rro/views.py
T
2026-09-21 20:37:18 +03:00

301 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from datetime import datetime
import json
from django.contrib.auth.decorators import login_required
from django.core.exceptions import ValidationError
from django.core.serializers.json import DjangoJSONEncoder
from django.db import IntegrityError, transaction
from django.db.models import Q
from django.http import HttpResponseForbidden
from django.shortcuts import get_object_or_404
from django.views.decorators.http import require_POST
from apps.locations.models.pharmacies import Pharmacy
from core.models.directories import Directory
from core.portal_access import section_required
from core.utils.htmx import render_page
from core.location_access import accessible_pharmacies, scoped_queryset, has_all_locations
from .models import CashRegister
from .access import (
can_edit_rro_registers,
user_can_access_register,
)
def _parse_date(raw):
raw = (raw or '').strip()
if not raw:
return None
try:
return datetime.strptime(raw, '%Y-%m-%d').date()
except ValueError:
raise ValidationError({'__all__': ['Некорректная дата.']})
def _parse_register_form(post, *, instance=None):
"""Збирає поля з POST у dict для CashRegister + ValidationError map."""
errors = {}
pharmacy_id = post.get('pharmacy') or post.get('pharmacy_id') or None
if not pharmacy_id:
errors['pharmacy'] = ['Выберите аптеку.']
else:
try:
pharmacy_id = int(pharmacy_id)
except (TypeError, ValueError):
errors['pharmacy'] = ['Некорректная аптека.']
kind = (post.get('kind') or '').strip()
if kind not in {CashRegister.Kind.RRO, CashRegister.Kind.PRRO}:
errors['kind'] = ['Выберите тип (РРО / ПРРО).']
fiscal_number = (post.get('fiscal_number') or '').strip()
if not fiscal_number:
errors['fiscal_number'] = ['Укажите фискальный номер.']
status_id = post.get('status') or post.get('status_id') or None
if not status_id:
errors['status'] = ['Выберите статус.']
else:
try:
status_id = int(status_id)
except (TypeError, ValueError):
errors['status'] = ['Некорректный статус.']
vendor_raw = post.get('vendor') or post.get('vendor_id') or ''
vendor_id = None
if str(vendor_raw).strip():
try:
vendor_id = int(vendor_raw)
except (TypeError, ValueError):
errors['vendor'] = ['Некорректный вендор.']
try:
registered_at = _parse_date(post.get('registered_at'))
cert_expires_at = _parse_date(post.get('cert_expires_at'))
except ValidationError as exc:
errors.update(exc.message_dict if hasattr(exc, 'message_dict') else {'__all__': list(exc.messages)})
registered_at = None
cert_expires_at = None
data = {
'pharmacy_id': pharmacy_id,
'kind': kind,
'fiscal_number': fiscal_number,
'serial_number': (post.get('serial_number') or '').strip(),
'model_name': (post.get('model_name') or '').strip(),
'vendor_id': vendor_id,
'status_id': status_id,
'registered_at': registered_at,
'cert_expires_at': cert_expires_at,
'is_primary': bool(post.get('is_primary')),
'provider': (post.get('provider') or '').strip() if kind == CashRegister.Kind.PRRO else '',
'notes': (post.get('notes') or '').strip(),
}
form_data = {
**{k: ('' if v is None else v) for k, v in data.items()},
'registered_at': post.get('registered_at') or '',
'cert_expires_at': post.get('cert_expires_at') or '',
'is_primary': data['is_primary'],
'pharmacy': data['pharmacy_id'] or '',
'status': data['status_id'] or '',
'vendor': data['vendor_id'] or '',
}
if errors:
raise ValidationError(errors)
if instance is None:
register = CashRegister(**data)
else:
register = instance
for key, value in data.items():
setattr(register, key, value)
try:
register.full_clean()
except ValidationError:
raise
return register, form_data
def _rro_list_context(request, **extra):
user = request.user
can_edit = can_edit_rro_registers(user)
full_registry = True # фильтры доступны и для нескольких закреплённых аптек
registers = scoped_queryset(user, CashRegister.objects.all(), 'pharmacy').select_related(
'pharmacy', 'pharmacy__organization', 'vendor', 'status',
)
filter_pharmacy = request.GET.get('pharmacy', '')
filter_org = request.GET.get('organization', '')
filter_kind = request.GET.get('kind', '')
filter_status = request.GET.get('status', '')
q = (request.GET.get('q') or '').strip()
show_create = request.GET.get('new') == '1'
if full_registry:
if filter_pharmacy:
registers = registers.filter(pharmacy_id=filter_pharmacy)
if filter_org:
registers = registers.filter(pharmacy__organization_id=filter_org)
if filter_kind in {CashRegister.Kind.RRO, CashRegister.Kind.PRRO}:
registers = registers.filter(kind=filter_kind)
if filter_status:
registers = registers.filter(status_id=filter_status)
if q:
registers = registers.filter(
Q(fiscal_number__icontains=q)
| Q(serial_number__icontains=q)
| Q(model_name__icontains=q)
| Q(provider__icontains=q)
)
statuses = Directory.objects.filter(
directory_type__code='rro_status', is_active=True,
).order_by('name')
vendors = Directory.objects.filter(
directory_type__code='rro_vendor', is_active=True,
).order_by('name')
register_list = list(registers[:500])
registers_js = [
{
'id': reg.pk,
'pharmacy': str(reg.pharmacy_id),
'kind': reg.kind,
'fiscal_number': reg.fiscal_number,
'serial_number': reg.serial_number,
'model_name': reg.model_name,
'vendor': str(reg.vendor_id) if reg.vendor_id else '',
'status': str(reg.status_id),
'registered_at': reg.registered_at.isoformat() if reg.registered_at else '',
'cert_expires_at': reg.cert_expires_at.isoformat() if reg.cert_expires_at else '',
'provider': reg.provider,
'is_primary': reg.is_primary,
'notes': reg.notes,
}
for reg in register_list
]
context = {
'rro_full_registry': full_registry,
'is_staff': full_registry, # совместимость шаблона
'can_edit_rro': can_edit,
'rro_pharmacy_scoped': not has_all_locations(user),
'registers': register_list,
'registers_json': json.dumps(registers_js, cls=DjangoJSONEncoder, ensure_ascii=False),
'statuses': statuses,
'vendors': vendors,
'kind_choices': CashRegister.Kind.choices,
'filter_pharmacy': filter_pharmacy,
'filter_org': filter_org,
'filter_kind': filter_kind,
'filter_status': filter_status,
'filter_q': q,
'show_create_panel': show_create and can_edit,
'prefill_pharmacy': filter_pharmacy if can_edit else '',
'all_pharmacies': (
accessible_pharmacies(user).filter(is_active=True)
.select_related('organization')
.order_by('organization__name', 'pharmacy_number')
if full_registry else []
),
'organizations': (
Directory.objects.filter(pk__in=accessible_pharmacies(user).values('organization_id'), is_active=True).order_by('name')
if full_registry else []
),
}
context.update(extra)
# Explicit open create panel on validation errors
if context.get('rro_form_errors') and not context.get('rro_edit_id'):
context['show_create_panel'] = True
if context.get('rro_form_data'):
context['rro_form_data_json'] = json.dumps(
context['rro_form_data'], cls=DjangoJSONEncoder, ensure_ascii=False,
)
return context
def _render_rro(request, **extra):
from core.accounting_portal import render_accounting_portal
return render_accounting_portal(request, 'rro', **extra)
@login_required
@section_required('rro')
def rro_list_view(request):
"""Реестр РРО/ПРРО по аптекам. Staff видит все; сотрудник аптеки — свою."""
return _render_rro(request)
@login_required
@section_required('rro')
@require_POST
def rro_create_view(request):
if not can_edit_rro_registers(request.user):
return HttpResponseForbidden()
form_data = dict(request.POST.items())
try:
register, form_data = _parse_register_form(request.POST)
if not user_can_access_register(request.user, register):
return HttpResponseForbidden()
with transaction.atomic():
register.save()
return _render_rro(request, rro_form_message='Кассовый аппарат добавлен')
except ValidationError as exc:
return _render_rro(
request,
show_create_panel=True,
rro_form_errors=exc.message_dict if hasattr(exc, 'message_dict') else {'__all__': list(exc.messages)},
rro_form_data=form_data,
)
except IntegrityError:
return _render_rro(
request,
show_create_panel=True,
rro_form_errors={'fiscal_number': ['Фискальный номер уже используется.']},
rro_form_data=form_data,
)
@login_required
@section_required('rro')
@require_POST
def rro_update_view(request, pk):
if not can_edit_rro_registers(request.user):
return HttpResponseForbidden()
register = get_object_or_404(scoped_queryset(request.user, CashRegister.objects.all(), 'pharmacy'), pk=pk)
if not user_can_access_register(request.user, register):
return HttpResponseForbidden()
form_data = dict(request.POST.items())
try:
register, form_data = _parse_register_form(request.POST, instance=register)
if not user_can_access_register(request.user, register):
return HttpResponseForbidden()
with transaction.atomic():
register.save()
return _render_rro(request, rro_form_message='Изменения сохранены')
except ValidationError as exc:
return _render_rro(
request,
rro_edit_id=pk,
rro_form_errors=exc.message_dict if hasattr(exc, 'message_dict') else {'__all__': list(exc.messages)},
rro_form_data=form_data,
)
except IntegrityError:
return _render_rro(
request,
rro_edit_id=pk,
rro_form_errors={'fiscal_number': ['Фискальный номер уже используется.']},
rro_form_data=form_data,
)