301 lines
11 KiB
Python
301 lines
11 KiB
Python
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,
|
||
)
|